#Shadow DOM Imperative API Brainstorming
##Reduced test case:
<div>
<template shadow> <!-- shadow root -->
<details>
<content></content>
</details>
</template>
<summary>SUMMARY</summary>
<p>ALSO STUFF</p>
</div>
<summary>
and <details>
are used here because their behavior is understood. One can imagine any widget with the same behavior in their place. For the sake of this exercise, the <details>
has the following shadow tree:
<content id="summary-ui" select="summary:first-of-type"></content>
<div>
<content id="everything-else"></content>
</div>
Steps:
- Suppose you start with no
<summary>
element in the tree - At this point, the
<details>
sees nothing and displays nothing. - The
<summary>
is created and inserted into the tree. - Somewhere after this point, but prior to rendering, the two insertion points in
<details>
need to be informed that<summary>
is now available as a distribution candidate. - The
content#summary-ui
looks at the order of<summary>
relative to other elements in the distribution pool and only picks it if it's the first<summary>
. - The
content#everything-else
grabs leftovers of the distribution pool.
##Rough Ideas
Here are the ideas we explored.
###Distribution Callback
The distribution callback is called at the time of distribution (maybe at the time of pool distribution):
function distributionCallback(pool) {
return filter(pool);
}
What's unclear is when to run this callback.
div.appendChild(summary); // callback runs here?
var top = summary.offsetTop; // callback runs here?!
assert(top);
The preferred option is to run distribution callbacks as lazily as possible, to avoid churn of distribution on every DOM operation. Today, this timing in Blink coincides with style resolution, and it works well. Unfortunately, this means that function getters that force style updates will now run arbitrary JS code.
Formulated generally, the imperative API needs to be able to react to changes in composition and affect box tree construction.
###Passive Candidate Array
An array of items on each insertion point, which can be populated at will by the author. This array is then used as the candidate pool the time of distribution. It's the responsibility of the author to update the array in reaction to DOM changes.
In this situation, an insertion point needs to effectively re-implement distribution algorithms by hand:
When <summary>
is inserted in steps above, the only way for <details>
for to detect this and react to it is by installing mutation observers on the document. In the general case, a widget needs to always watch children of ancestors with shadow roots, then figuring out whether they will be distributed all the way down to the widget, and then update the pool accordingly. This seems extra-super-duper-bad.
###Selector-based Routing
Another idea would be to provide a more explicit way to define distributions via a selector-based routers:
var router == new DistributionRouter("summary:first-of-type");
router.takeChildrenFrom(div)
router.distributeTo(contentSummaryUI);
This is a bit of a cop-out, since it does not allow explicitly listing elements as candidates, but it does not have the drawbacks of the previously listed ideas.
The simplest solution here seems to be to simply have an API like
partial interface HTMLContentElement { insertionFor(Element child); // To be bikeshed };
The caller can then choose to run this whenever is appropriate. I.e. by listening to mutation observers and/or layout related events.
I guess you'd also need to be able to be notified about when the list of nodes distributed into a changes. So that you can deal with nested insertion points... So the problem comes back to when such an event fires.