HTML Parts and Walls


#1

This is a spec I’m currently drafting at https://github.com/stuartpb/pwalls-spec to solve a widespread problem many other developers and I have encountered with the current Web platform, in a simple, prollyfillable fashon that, unlike Web Components, doesn’t require widespread overhauls/throw-outs of an app/developer’s codebase, toolchain, organization pattern, or encapsulation model.

You can read the proposal on GitHub - rather than maintain a copy of the proposal in two places (GitHub and this post), I’m going to keep the proposal in that location up-to-date.

Some differences from when I initially wrote my draft and when I most recently edited this post (seen in the replies below):

  • The Walled Descendant Selector (originally called the Proot Boundary Selector) was originally suggested as /, not |> as it is in the current spec.
  • The original spec made a lot more references to Shadow DOM, which were removed because they made it seem like it was a declarative-DOM-encapsulation proposal.

Getting rid of cookie warnings
#2

Without commenting on the rest (there’s a lot to take in), something that jumped out at me at first glance is I think that the Part Boundary Selector is problematic/needs tweaking… Slash is already reserved for combinators of the style /foo/ (both sides slash)… It’s a little tough to know what to suggest you change it to because I’m not sure what it ‘is’ here - it’s not a pseudo class or pseudo element, it’s not exactly like a combinator as described because somehow you are also using it to identify the thing itself too… I suppose you could pick one of the few remaining characters left and invent something new but I don’t see anyone wanting to give up $ for example or wanting to invent a new concept within selectors to implement if we can somehow reasonably fit it into something existing… can we?


#3

I was thinking that avoiding the conflict with that is feasible, as all the uses of that reference combinator that I’ve seen have used (what I’m assuming is mandatory) whitespace on both sides. This would dictate the behavior of a single slash immediately following a selector, with no intervening space.

If the space isn’t mandatory, this could be replaced with some arrow-like selector (eg. => or, since /deep/ has been walked back, >>>).


#4

TL;DR:

  • Two new global attributes, root and part
  • Elements with root count as a base / boundary when getting elements by part in JS
  • CSS descendant selector for elements within root (and/or Shadow DOM) boundaries
    • Within bounded descendant selector, # matches part as well as id

#5

It is exceedingly unlikely that something that was similar to, but wholly distinct from, Shadow DOM will have any traction whatsoever. Nobody wants to implement two completely separate, but mostly overlapping, APIs if they don’t have to.

Instead, helping to come up with ways to make Shadow DOM easier to use and solve other use-cases would be far more productive.

Some specific points:

  • the /deep/ combinator and ::shadow pseudo-element were purposely removed, because they are crazy performance killers, and actually had fairly limited purpose; most styling either takes place within the component, or can be done via custom properties inheriting in.

    We plan to introduce ::part() or something like it in the future, once things finish settling down and get agreed on, to allow for a different kind of shadow-piercing. (Variables let you pierce arbitrary values through a shadow, but the component has to predeclare what things will use variables. It’s best for adjusting things that are used repeatedly throughout a component, but bad for having particular parts of a component be highly customizable. ::part() should be the opposite - ideal for making particular elements fully stylable, but terrible for overall theming.)

  • Having getElementsByTagName() and querySelector() see different things and return different results would be very confusing. Having querySelector() and CSS Selectors see different things and return different results would be very confusing. By transitivity, all three need to have a consistent view of the DOM. Without this, libraries will get very confused; today it doesn’t matter which of the three methods are used to apply styles in the DOM.

  • Declarative shadow DOM is planned for the future. Again, we’re waiting for agreement and general interop for the basics first, getting the minimum viable product working well before we start tacking on nice-to-have features.

  • I don’t understand what you mean by the “indexing elements within” section.


#6

They’re not “mostly overlapping”, though. Shadow DOM cuts you off from a lot of techniques page authors have been using to build their pages, and will continue to use to build their pages. Unlike Shadow DOM, these part and root attributes do not impact existing behaviors of elements within pages when an element uses them. All the normal, working-joe content authors I’ve shown this to have said it makes more sense to them and their workflow than Shadow DOM.

Yes, which is why neither of those things happen. I think you might have misread the spec. This does not change the behavior of querySelector or getElementsByTagName. It adds a new descendant selector, which works analogously to the getPart() function. When not using this descendant selector, nothing changes. (Again, I think you misunderstood and thought this is supposed to be something like “Declarative Shadow DOM”, which it’s not.)

If you’re talking about this:

What I mean is you either have to use id, which is going to break if you use the HTML for your component in any context other than its own Shadow DOM, or you use class, which requires the really kludgy getElementsByClassName('name-of-part')[0]. (Also, every sub-component of your HTML is going to have to face one of these two restrictions as well - so you’re either juggling a half-dozen Shadow DOMs just to make a nametag, or you’re stuck with the same problems of CSS namespacing this was supposed to ameliorate.)


#7

Ah, I did indeed completely misunderstand the proposal. Now that I know what it is, I see the benefit a bit more.

Correct me if this summary is wrong: This is a way to canonicalize BEM-style component-part-naming standards into a more usable, more dependable mechanism. When you use the “component” combinator, it only selects elements within the component, not those inside of further-nested components.

If this is correct, then I see the value and kinda like it. I’m not sure I understand the value of adding part as an alternate ID (we can just stick with classes; just limit yourself to one use if you want it to be unique), but otherwise it’s pretty cool.


#8

I had the same thought coming into this, and here are the two reasons I went this way:

  • using part specifically signals to validators or whatever that the attribute value is supposed to appear only once within the proot boundary (I’m using the term “proot boundary” instead of “component’s scope” for the moment to avoid confusion with other ways components can historically and speculatively be defined).
    • Beyond mere validation, having it as a distinct uniqueness-oriented namespace enables doing things like gathering all elements with a part attribute by name into a parts object-property on the proot element (or a special parts-only tree view eg. in DevTools), which would be less practical to do for all the defined class names.
  • using part lets you not have to worry about collisions in the global class namespace (ie. if I’m thinking about making a global “gauge” class, I don’t have to worry about how it will interact with any time I’ve ever used the name “gauge” as a part name within a component).

EDIT: Also, using part allows for getPart to be optimized similarly to getElementById, rather than getElementsByClassName.


#9

Can you explain how this might be different than adding in namespaces into tags?

I get that:

  • Tags would need to be defined with new namespaces
  • That yours allows immediate access to adding roots

However I feel namespaces give the same level of abstraction in terms of selecting elements and also specificity.

Also I’m pretty sure there was some rumblings of adding that back into custom elements.

Also yeah root isn’t a good name to use here if this happens as you mentioned.


#10

The big difference that makes this immediately work for me is that this doesn’t require new tags, at all. The root of your “component” with this can be an ordinary HTML tag with all its semantics ready-to-go, like div, or select, or form, or picture (which is usually all I really want - just a way of identifying and styling child elements underneath a parent element, without needing to write or formulate a script for “registration” constructions).


#11

I like that part of it certainly.

So if all CSS used the root based selectors then there would be no CSS leakage? So in a external widget implementation it would be similar to them using scoped styles right?

Does normal CSS impact the root’s? Would the following CSS impact your example?:

div div {color:red !important;}

#12

When using only proot-bounded-descendant-selectors, yes. An example of a stylesheet that could be applied to style the example HTML in the OP (using => as the PRBDS):

.lotto=>.lid {
  color: #800;
}
.lotto=>#timer {
  font-size: 24px;
}
.lotto=>#bubble {
  background-color: #0ff; border-radius: 999px;
}
.lotto=>#track {
  /* would only affect example-chosen-track,
     without leaking to example-running-track */
  border-bottom: 4px solid #999;
}
.lotto=>#bubble=>#track {
  /* would only affect example-running-track */
  border-bottom: 4px solid #999;
}
.lotto=>#bubble=>#funnel {
  /* would affect example-bubble-funnel */
  background: #444;
}
.lotto=>#funnel {
  /* would not affect anything because
     example-lotto-bubble is a proot boundary
     which is not crossed by => */
  border: red; color: red; background: red;
}

(This stylesheet would style elements with the lotto class name anywhere on the page: if you wanted to restrict it from use in sub-element contexts, you could prefix .lotto itself with =>, though this is not currently in the spec.)

Parts and roots do not change the behavior of normal CSS. This would apply the exact same way as if the part/root attributes were not present.


#13

Since “root” is used in so many other places in the DOM (and it introduces undue conflation with things like Shadow Roots), I’ve renamed everything that was described as a root or “proots” in this spec to walls (with the attribute now being wall). On top of being less likely to collide with names used for other aspects of elements, I think this also better conveys the purpose of this mechanism.

I’ve also changed the proposed “walled descendant” selector to use the otherwise-invalid |> sequence, to avoid issues with /.


#14

I’ve written a proof-of-concept first-pass prollyfill for this spec: https://github.com/stuartpb/partywall


#15

I’ve migrated the much-smaller DOM aspects of the aforementioned polyfill to their own polyfill: https://github.com/stuartpb/getpart-polyfill


#16

The next level of tooling I’d like to see for this is some plugin for Grunt / Gulp / whatever the kids are using these days, converting stylesheets written using the |> Walled Descendant Selector (using postcss/autoprefixer or similar) into partywall-compatible .parent[data-partywall-level="0"][wall] .child[data-partywall-level="1"]-type rules (which the prollyfill then recognizes after the browser loads the stylesheet, and extends in the event that elements get created at further depths than the statically-generated rules include).

This could also be combined with an HTML post-processor (and/or middleware for Express) that loads the data-partywall-level attributes onto elements in static HTML files (or rendered responses), so that the aforementioned styles apply immediately on first load, rather than waiting for the prollyfill to add the necessary depth attributes for matching (or breaking if the user has scripts disabled).