joicomponents
Version:
Design patterns to build native web components
153 lines (143 loc) • 6.37 kB
HTML
<script>
function simplifiedFlatDomChildNodes(slot){
const flattenedChildNodes = slot.assignedNodes({flatten: true});
if (flattenedChildNodes.length === 0)
return slot.childNodes;
const flatFlat = [];
for (let node of flattenedChildNodes) {
if (node.tagName === "SLOT"){
for (let child of node.childNodes)
flatFlat.push(child);
} else {
flatFlat.push(node);
}
}
return flatFlat;
}
class SisyphusList extends HTMLElement {
constructor() {
super();
this.now = new Date();
this.dayMonth = "day";
this.attachShadow({mode: "open"});
this.shadowRoot.innerHTML = `
<slot>
<sisyphus-item id="one">roll stone up the hill</sisyphus-item>
<sisyphus-item id="two">roll stone up the hill</sisyphus-item>
<sisyphus-item id="three">roll stone up the hill</sisyphus-item>
</slot>
`;
console.log("constructor() driven processFlatDomChildren: ", this.dayMonth);
this.processFlatDomChildren(this.shadowRoot.children[0].childNodes);
this.shadowRoot.addEventListener("slotchange", function(e){
console.log(this.id);
debugger;
console.log("slotchange driven processFlatDomChildren: ", this.dayMonth);
this.processFlatDomChildren(simplifiedFlatDomChildNodes(this.shadowRoot.children[0]));
}.bind(this));
}
static get observedAttributes(){
return ["day-month"];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === "day-month"){
this.dayMonth = newValue;
console.log("attribute() driven processFlatDomChildren: ", this.dayMonth);
this.processFlatDomChildren(simplifiedFlatDomChildNodes(this.shadowRoot.children[0]))
}
}
processFlatDomChildren(childNodes){
let i = 0;
for (let child of childNodes) {
if (child.tagName && child.tagName === "SISYPHUS-ITEM"){
if (this.dayMonth === "day") {
let time = new Date(this.now);
time.setDate(time.getDate()+i++);
child.updatePlan(time.toLocaleDateString("en-US", {weekday: "short"}));
}
else if(this.dayMonth === "month") {
let time = new Date(this.now);
time.setMonth(time.getMonth()+i++);
child.updatePlan(time.toLocaleDateString("en-US", {month: "short"}));
}
}
}
}
}
class SisyphusItem extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: "open"});
this.shadowRoot.innerHTML = `
<style>
:host(*){
display: block;
}
</style>
<span>§*/: </span>
<slot>How nice, nothing to do!</slot>
`;
}
updatePlan(txt){
this.shadowRoot.querySelector("span").innerText = txt + ": ";
const child = simplifiedFlatDomChildNodes(this.shadowRoot.children[2])[0];
}
}
customElements.define("sisyphus-list", SisyphusList);
customElements.define("sisyphus-item", SisyphusItem);
</script>
<sisyphus-list id="lifeInGeneral"></sisyphus-list>
<sisyphus-list id="lifeAsAWebDeveloper" day-month="month">
<sisyphus-item id="a">understand the SlotMatroska</sisyphus-item>
<sisyphus-item id="b">understand the SlotMatroska</sisyphus-item>
<sisyphus-item id="c">understand the SlotMatroska</sisyphus-item>
</sisyphus-list>
<h3>SlotchangeNipSlip #3</h3>
This example illustrate the problems of viewing fallback nodes as the default state of a web component and _not_
giving a slotchange event when a slot is declared using its fallback nodes.
<ol>
<li>
During constructor(): The default state needs to be processed, because the relationship to the
nested web component depends on an context dependent state (this.now). Although the relationship
between sisyphus-list and sisyphus-item is known, this relationship builds on contextual data
which is not accessible in HTML nor CSS template.
</li>
<li>
Both the sisyphus-list elements process this initial state. The first list, #lifeInGeneral,
needs and will use the result of this process. But as we will see, #lifeAsAWebDeveloper does here
do redundant work.
</li>
<li>
During attributeChangedCallback(): The default state needs to be processed again because the
nested web components needs to be processed based on a per element specific state (this.dayMonth).
If no attribute is set, no attributeChangedCallback() will be triggered. Without manually
postponing the processing task, which is far from trivial, #lifeAsAWebDeveloper will here perform
first re-processing of the sisyphus-items, making previous processing redundant.
</li>
<li>
During slotchange event listener: The second sisyphus-list, #lifeAsAWebDeveloper, however gets
a slotchange event. In fact, it can somewhere between 4, 6 or 7, depending on the browser and
your "debugger;" statements. What?! 4, 6 _or_ 7 slotchange events?! Depending on "debugger;"?!
Yes... The mayhem that is RedundantSlotchangeCreations is discussed in SlotchangeNipSlip #4.
For now, we simply say that at least one slotchange event was triggered.
</li>
<li>
The slotchange event listener will again need to reprocess the sisyphus-items, making the two
previous processes of sisyphus-items redundant.
</li>
<li>
The SlotchangeNipSlip #3 problem is that:
a) since sisyphus-list#lifeInGeneral gets no slotchange event nor attributeChangedCallback(),
the sisyphus-list web component needs to process its flattened DOM children from the constructor().
b) sisyphus-list#lifeAsAWebDeveloper is instantiated with both an attribute and transposed nodes.
This means that the sisyphus-list#lifeAsAWebDeveloper gets an additional attributeChangedCallback and at least
on slotchange callback. sisyphus-list#lifeAsAWebDeveloper only needs process the last of these
triggers.
</li>
<li>
As we will see later, the solution to SlotchangeNipSlip #3 is to control the timing of the
slotchange reaction better, and to trigger the same reaction whenever a slot element instantiates
itself (regardless of initial state). This will patch the problem of
MissingInitialFallbackNodeSlotchange, along with the other SlotchangeNipSlip problems.
</li>
</ol>