joicomponents
Version:
Design patterns to build native web components
203 lines (176 loc) • 7.59 kB
Markdown
# Problem: PrematureSlotchange
When the browser creates a web component that use another web component in its shadowDOM,
then the `slotchange` events of the inner web components are triggered *before* the declared DOM
is complete. This means that when some `slotchange` events are triggered, they will run against a
DOM branch that essentially *doesn't exist* from an HTML perspective. We call this the
PrematureSlotchange problem.
In [Theory: DomNodesAndBranch](7_Theory_DomNodesAndBranch), we discussed the theoretical background
for this problem. In this chapter we will exemplify what it looks like. We start with a slightly
modified version of our `<grand-father>` example.
## Example: `<grand-father>`
```html
<script>
function log(e){
const transposedNodes = e.target.assignedNodes({flatten: true});
let output = "";
if (transposedNodes.length)
output = transposedNodes[0].data;
console.log(this.tagName + ": " + e.target.tagName + "#" + e.target.id, " => " + output);
}
class GrandFather extends HTMLElement {
constructor(){
super();
this.attachShadow({mode: "open"});
this.shadowRoot.innerHTML = "<the-father><slot id='grandFather'></slot></the-father>";
this.shadowRoot.addEventListener("slotchange", log.bind(this));
}
}
class TheFather extends HTMLElement {
constructor(){
super();
this.attachShadow({mode: "open"});
this.shadowRoot.innerHTML = "<the-child><slot id='father'></slot></the-child>";
this.shadowRoot.addEventListener("slotchange", log.bind(this));
}
}
class TheChild extends HTMLElement {
constructor(){
super();
this.attachShadow({mode: "open"});
this.shadowRoot.innerHTML = "<slot id='child'></slot>";
this.shadowRoot.addEventListener("slotchange", log.bind(this));
}
}
customElements.define("grand-father", GrandFather);
customElements.define("the-father", TheFather);
customElements.define("the-child", TheChild);
</script>
<grand-father>matroska</grand-father>
<h3>Explanation: </h3>
The flattened DOM looks like this:
<pre>
...
< grand-father >
#shadowRoot
< the-father >
#shadowRoot
< the-child >
#shadowRoot
< slot#child >
< slot#father >
< slot#grandFather >
matroska
</pre>
Constructing this DOM produces six logs:
<pre>
THE-CHILD: SLOT#father =>
THE-FATHER: SLOT#father =>
THE-CHILD: SLOT#child =>
THE-CHILD: SLOT#grandFather => matroska
THE-FATHER: SLOT#grandFather => matroska
GRAND-FATHER: SLOT#grandFather => matroska
</pre>
A view of the DOM in which the three slotchange events are executed.
<pre>
in-complete DOM completed DOM
< grand-father > § < grand-father > _
#shadowRoot § #shadowRoot *6
< the-father > _ § < the-father > |
#shadowRoot *2 § #shadowRoot *5
< the-child > | _ § < the-child > |
#shadowRoot *1 *3 § #shadowRoot *4
< slot#child > | ^ § < slot#child > |
< slot#father > ^ § < slot#father > |
< slot#grandFather > § < slot#grandFather > ^
"" § matroska
</pre>
<ol>
<li>
When log 1, 2, and 3 run, the DOM is in a temporary state.
The grand-father element is created, but it does not have any child nodes yet:
the "matroska" text node is not yet registered as its child.
This means that when the log functions run assignedNodes({flatten: true}), it is empty.
</li>
<li>
When log 4, 5, and 6 run, the DOM is completed. The "matroska" text node is now registered
as a child of the grand-father host element, and the assignedNodes({flatten: true})
return the anticipated result "matroska".
</li>
</ol>
```
## `slotchange` triggered on incomplete DOM branches
When the `<grand-father>` element is declared, its constructor creates a second web component,
`<the-father>` in its shadowDOM. `<the-father>` in turn creates a third web component in its shadowDOM,
`<the-child>`. At *the same time* a text node child "matroska" is declared a child of the
`<grand-father>` element.
From the perspective of HTML, these operations *all* happen *at the same time*. They are
instantenous, declaratively synchronous. From the perspective of HTML, it therefore *never* exists
a DOM branch that looks like this:
```
<grand-father></grand-father>
```
but only a DOM branch that looks like this:
```
<grand-father>matroska</grand-father>
```
However, the first *two* `slotchange` events are dispatched at a time when "matroska" is not a child
of the `<grand-father>` element. The first two `slotchange` events occur at a time that doesn't exist
in the world of HTML.
One might argue that this context *do* exist in JS time. And practically, as `slotchange` currently
function, one would of course be correct. It does.
But. This is wrong. No DOM branch reaction should be triggered based on a DOM branch that does not
exist in *both* HTML and JS time. Web component reactions, ie. DOM node reactions, *can* trigger at
such temporary stages because they *should not* rely on data from the temporary, incomplete DOM
branches. The state of the surrounding DOM *should not* matter to them, and therefore they can be run
earlier. But `slotchange` is a DOM branch reaction. It relies on data about the DOM branch context
that surrounds it. And therefore it *should not* be run until this DOM branch context is complete
*both* in HTML and JS time.
The solution to this problem is to *delay* the reactions of `slotchange` event listener until the
earliest, appropriate point when the DOM branch is complete both from an HTML and JS perspective.
In later chapters about the `slotCallback`, I will do just that.
## `<script>` trigger `slotchange` events too
There is another way to trigger `slotchange` events to be dispatched prematurely: `<script>`s.
Below is an example to illustrate how and when.
## Example: ScriptTriggerSlotchange
```html
<script>
class GreenFrame extends HTMLElement {
constructor(){
super();
this.attachShadow({mode: "open"});
this.shadowRoot.innerHTML =
`<style>
div {
border: 10px solid green;
}
</style>
<div>
<slot>This is life!</slot>
</div>`;
const slot = this.shadowRoot.children[1].children[0];
this.shadowRoot.addEventListener("slotchange", function(){
console.log(slot.assignedNodes({flatten: true}));
});
}
}
customElements.define("green-frame", GreenFrame);
</script>
<green-frame>
<script>theSunIsShining();</script>
<pre>¯\_(ツ)_/¯</pre>
<script>takeABreak;</script>
<pre> | | </pre>
<script>relax;</script>
<pre> | 6 | </pre>
<script>putYourFeetUp;</script>
<pre> === </pre>
<script>enjoyLife;</script>
<pre> | | </pre>
<script>letSomeOneElseDoTheJob();</script>
<pre> | | | </pre>
<script>tomorrow === aNewDay;</script>
<pre> ^ ^ </pre>
</green-frame>
```
## References
*