joicomponents
Version:
Design patterns to build native web components
195 lines (175 loc) • 5.43 kB
HTML
<template id="portrait">
<style>
green-frame {
display: block;
position: relative;
width: 300px;
height: 300px;
}
blue-frame {
position: absolute;
bottom: 0;
left: 0;
display: inline-block;
box-sizing: border-box;
width: 100%;
}
</style>
<green-frame>
<slot id="portraitOne">a picture</slot>
<blue-frame>
<slot id="portraitTwo" name="label">of something</slot>
</blue-frame>
</green-frame>
</template>
<template id="green">
<style>
:host {
border: 4px solid green;
}
</style>
<slot id="green"></slot>
</template>
<template id="blue">
<style>
:host {
border: 4px solid blue;
}
</style>
<slot id="blue"></slot>
</template>
<script>
function isNipSlip(composedPath, shadowRoot){
for(let node of composedPath){
if (node.tagName !== "SLOT")
return true;
if (node.getRootNode() === shadowRoot)
return false;
}
return true;
}
startLog = false;
function log(e) {
startLog && console.log(this.tagName, e.composedPath(), isNipSlip(e.composedPath(), this.shadowRoot));
}
class PortraitFrame extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: "open"});
const template = document.querySelector("#portrait").content.cloneNode(true);
this.shadowRoot.appendChild(template);
this.shadowRoot.addEventListener("slotchange", log.bind(this));
}
}
class GreenFrame extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: "open"});
const template = document.querySelector("#green").content.cloneNode(true);
this.shadowRoot.appendChild(template);
this.shadowRoot.addEventListener("slotchange", log.bind(this));
}
}
class BlueFrame extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: "open"});
const template = document.querySelector("#blue").content.cloneNode(true);
this.shadowRoot.appendChild(template);
this.shadowRoot.addEventListener("slotchange", log.bind(this));
}
}
customElements.define("blue-frame", BlueFrame);
customElements.define("green-frame", GreenFrame);
customElements.define("portrait-frame", PortraitFrame);
</script>
<portrait-frame>¯\_(ツ)_/¯</portrait-frame>
<script>
startLog = true; //has skipped logs during construction, starts log here
const span = document.createElement("span");
span.setAttribute("slot", "label");
span.innerText = "SlotMatroska";
document.querySelector("portrait-frame").appendChild(span);
</script>
<h3>Explanation:</h3>
The four normal DOM documents look like this (excluding < style > elements):
<pre>
1.---------------------------
index.html
...
< portrait-frame >
"¯\_(ツ)_/¯"
...
2.---------------------------
portrait-frame#shadowRoot
< green-frame >
< slot#portraitOne >
"a picture"
< blue-frame >
< slot#portraitTwo name="label" >
"of something"
3.---------------------------
green-frame#shadowRoot
< slot#green >
4.---------------------------
blue-frame#shadowRoot
< slot#blue >
</pre>
Adding the "matroska
Below is the flattened DOM _before_ "matroska" is added.
<pre>
...
< portrait-frame >
#shadowRoot
< green-frame >
#shadowRoot
< slot#green >
< slot#portraitOne >
"¯\_(ツ)_/¯"
< blue-frame >
#shadowRoot
< slot#blue >
< slot#portraitTwo >
"of something"
</pre>
Important to note here is:
<ol>
<li>
blue-frame element is _slotted_ into green-frame.
</li>
<li>
slot#portraitTwo is directly chained to slot#blue.
But! slot#portraitTwo is NOT directly chained to slot#green.
</li>
<li>
But since the host element blue-frame is put inside slot#green,
that means that also the blue-frame#shadowRoot and the SlotMatroska
it contains _also_ is contained within green-frame#shadowRoot and slot#green.
</li>
</ol>
When "matroska" is added, this changes the flattened DOM and causes a slotchange event
to be dispatched on slot#portraitTwo:
<pre>
...
< portrait-frame > _
#shadowRoot *3
< green-frame > |
#shadowRoot *2
< slot#green > |
< slot#portraitOne >
"¯\_(ツ)_/¯"
< blue-frame > |
#shadowRoot *1
< slot#blue > |
< slot#portraitTwo > slotchange ^
< span >
"matroska"
</pre>
And prints the following three logs:
<pre>
BLUE-FRAME [slot#portraitTwo, slot#blue, document-fragment, blue-frame, slot#green, document-fragment, green-frame, document-fragment]
GREEN-FRAME [slot#portraitTwo, slot#blue, document-fragment, blue-frame, slot#green, document-fragment, green-frame, document-fragment]
PORTRAIT-FRAME [slot#portraitTwo, slot#blue, document-fragment, blue-frame, slot#green, document-fragment, green-frame, document-fragment]
</pre>
When this slotchange event bubbles upwards, it passes *all* three #shadowRoot and slot#blue, slot#green, and slot#portraitTwo,
but not slot#portraitOne.