Bugzilla – Bug 15954
[Shadow]: The dilemma of document DOM tree mutations
Last modified: 2012-10-20 16:55:13 UTC
In experimenting with the tab manager (http://dglazkov.github.com/Tabs/), I stumbled across an interesting dilemma, which rises from pondering this question:
Should shadow DOM modify the document tree if the modification is only needed for the functional purposes of the shadow DOM subtree?
Consider this example:
In the tab manager, I need to style a currently selected tab. I have a couple of ways to do this.
a) Set a class on the currently selected tab. Since the tab is actually a combination of <h2> and <section> in the document tree, I must modify the document tree. This seems untidy, since the document DOM elements suddenly grow class names, and that increases the risk of unintended style-stomping.
b) Twiddle nth-of-type selectors. This is what I ended up doing. I set up a rule in the shadow DOM stylesheet: https://github.com/dglazkov/Tabs/blob/gh-pages/tabs-control.js#L23, and then tweak it every time the current tab changes: https://github.com/dglazkov/Tabs/blob/gh-pages/tabs-control.js#L121. Note that you have to pretend that bug 15946 is fixed, and rules are prefixed with "::content" selector.
The former is a lot easier to implement and more familiar to developers, but definitely leaks the abstraction.
The latter is ugly and unnatural, but it keeps the DOM tree pristinely clean. Outside of the shadow DOM, there are no DOM modifications happening.
Thus the dilemma: we must either ignore the leakage or adopt a gnarly pattern of dynamically mutating styles.
Perhaps there are other non-insance solutions here?
I don't think it's that bad to put a prefixed class or attribute in the light DOM.
I think the only real alternative is to have some way of producing style rules directly in JS that don't need to serialize properly, so you can just *set* its target to an element rather than using the indirection of crafting a selector. If you could do this in @select as well, you could cut out your whole indexOf thing.
(In reply to comment #1)
> I don't think it's that bad to put a prefixed class or attribute in the light
It may create styles to start matching unexpectedly. For instance, if I already had ".current" selector in my document making an element red and blinking with "This Just In:" generated content before it, setting a current tab to ".current" will make it look just like it. An expected result, and on top of that, since the component is encapsulating its logic, it's not knowable whether this could happen.
> I think the only real alternative is to have some way of producing style rules
> directly in JS that don't need to serialize properly, so you can just *set* its
> target to an element rather than using the indirection of crafting a selector.
> If you could do this in @select as well, you could cut out your whole indexOf
Additionally, we could imagine a "classThatOnlyMatchesInsideContent" attribute (needs better name!), which only matches as class when an element is distributed by a <content> element.
Yes, if you add something to the light DOM, it should be prefixed so that you have a reasonable expectation of avoiding collisions.
The idea of applying an "overlay" from the shadow to the light dom is interesting.
(In reply to comment #3)
> Yes, if you add something to the light DOM, it should be prefixed so that you
> have a reasonable expectation of avoiding collisions.
> The idea of applying an "overlay" from the shadow to the light dom is
don't SVGInstances have something like that?
No clue, but I don't know of any examples in SVG where their shadow dom includes some light dom. Their shadows are all leafs.
Okay, sketch for the overlay idea.
In early brainstorming discussions, we had an idea for letting shadow DOM expose only certain elements to outer-document selectors, by tagging an element with a @pseudo attribute (taking a string) which makes it selectable in the outer document with a selector like ".fancyComponent::pseudo(string)".
Let's work with that idea. Each shadow root gets access to an object that maps strings to elements. The elements can be from anywhere, shadow or light DOM. The ::pseudo() pseudo-element lets you select elements via this map. Light DOM elements, however, only match *if they were grabbed by a <content> in that component's shadow*.
This would solve the use-case nicely without having to pollute the light DOM or rely on hacky selector-editting. Just grab, in JS, the currently selected tab and pane. Stash both of them into the map under, say, "current-tab" and "current-pane". They're then easily selectable. When the user changes the active tab, just assign the new elements to the map, and it all works automatically with no cleanup necessary.
(In reply to comment #4)
> (In reply to comment #3)
> > Yes, if you add something to the light DOM, it should be prefixed so that you
> > have a reasonable expectation of avoiding collisions.
> > The idea of applying an "overlay" from the shadow to the light dom is
> > interesting.
> don't SVGInstances have something like that?
They don't do anything overlay-like. SVGElementInstance objects are just a tree of objects reflecting the structure of a shadow tree inside a <use> element, and letting you find which light DOM nodes the shadow nodes correspond to. (The actual shadow DOM nodes are always inaccessible in SVG currently.)
I like the idea of a DOMElementMap for pseudoids described in Comment 6.
Regarding the specific use case of a tab strip, would it be possible to merely reset a CSS variable on the ShadowRoot, for example:
var-selected-index: 2; // this concrete value changes in response to selection
and have the components styles refer to that?
See some discussion on the same subject here:
I guess my proposal and Tab's one are fairly similar (just to avoid you the pain of clicking, here's how it would look like):
/* CSS CODE */
@virtual-namespace tab-data "http://jquery.com/namespaces/tab-data";
/* JS CODE */
var tabData = getVirtualNamespace("http://jquery.com/namespaces/tab-data");
// add the virtual attribute to the element
tabData.set(element, "selected", "true");
// remove the virtual attribute
tabData.set(element, "selected", null);
It's also possible to use a normal namespace and suppose that leaking prefixed attributes is not an issue as they don't collide with natural attibutes. That solution is already OK and don't need any change from browser makers.