UNPKG

joicomponents

Version:

Design patterns to build native web components

355 lines (325 loc) 15.6 kB
import {flattenNodes} from "../../src/flattenNodes.js"; function testElementNodeListTagAndID(nodes, ar) { let tagIds = Array.from(nodes).map(n => { if (n.tagName) return n.tagName.toLowerCase() + (n.id ? "#" + n.id : ""); else return "text"; }); expect(tagIds).to.deep.equal(ar); } describe('flattenNodesTest basics', function () { it("flattenNodes() on single element", function () { const Subclass = class Subclass extends HTMLElement { }; customElements.define("flattened-children-1", Subclass); const el = new Subclass(); assert(flattenNodes(el.childNodes).length === 0); let child = document.createElement("div"); el.appendChild(child); el.appendChild(document.createElement("slot")); assert(flattenNodes(el.childNodes).length === 1); el.removeChild(child); assert(flattenNodes(el.childNodes).length === 0); }); it("The super inner-outer-slot test 1", function () { const InnerElementThatObserveChildren = class extends HTMLElement { }; const OuterElementThatSlotsStuff = class extends HTMLElement { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = ` <inner-flattened-children> <slot></slot> </inner-flattened-children> `; } }; customElements.define("inner-flattened-children", InnerElementThatObserveChildren); customElements.define("outer-flattened-children", OuterElementThatSlotsStuff); const outer = new OuterElementThatSlotsStuff(); const inner = outer.shadowRoot.children[0]; const innerSlot = inner.children[0]; //no div added as child, all slots are empty testElementNodeListTagAndID(flattenNodes(inner.childNodes), ["text", "text"]); testElementNodeListTagAndID(innerSlot.assignedNodes(), []); //add a div to outer let slotted = document.createElement("div"); outer.appendChild(slotted); testElementNodeListTagAndID(flattenNodes(inner.childNodes), ["text", "div", "text"]); testElementNodeListTagAndID(innerSlot.assignedNodes(), ["div"]); inner.removeChild(innerSlot); testElementNodeListTagAndID(flattenNodes(inner.childNodes), ["text", "text"]); testElementNodeListTagAndID(innerSlot.assignedNodes(), []); inner.appendChild(innerSlot); testElementNodeListTagAndID(flattenNodes(inner.childNodes), ["text", "text", "div"]); testElementNodeListTagAndID(innerSlot.assignedNodes(), ["div"]); }); /** * Example 1: `BucketList` * To illustrate the concept of lightDOM and shadowDOM, * we start with a simple example with low ambitions: `BucketList`. * The `BucketList` is a list of important things to do in life. * It anticipates to be filled with a series of `<div>`s with text, which it will center-align. * * So far, there is a simple distinction between the shadowDOM and the lightDOM * from the point of view of the `BucketList` element: * The shadowDOM is the DOM under the attached shadowRoot, ie. `style` and `slot`; * The lightDOM is the topDocument with its elements `bucket-list#list, div#one, div#two`. * We say that the `<slot>` transposes a set of actual elements from the lightDOM into the shadowDOM. */ it("BucketList test", function () { class BucketList extends HTMLElement { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = `<style id="sty">text-align: center;</style><slot></slot>`; } } customElements.define("bucket-list", BucketList); const topDocument = document.createElement("div"); topDocument.innerHTML = ` <bucket-list id="list"> <div id="one">fix bike</div> <div id="two">slice cucumbers</div> </bucket-list> `; const list = topDocument.querySelector("#list"); const listShadow = list.shadowRoot; const listShadowSlot = listShadow.children[1]; testElementNodeListTagAndID(flattenNodes(list.childNodes), ["text", "div#one", "text", "div#two", "text"]); testElementNodeListTagAndID(flattenNodes(listShadow.childNodes), ["style#sty", "text", "div#one", "text", "div#two", "text"]); testElementNodeListTagAndID(listShadowSlot.assignedNodes(), ["text", "div#one", "text", "div#two", "text"]); const one = topDocument.querySelector("#one"); list.removeChild(one); testElementNodeListTagAndID(flattenNodes(list.childNodes), ["text", "text", "div#two", "text"]); testElementNodeListTagAndID(flattenNodes(listShadow.childNodes), ["style#sty", "text", "text", "div#two", "text"]); testElementNodeListTagAndID(listShadowSlot.assignedNodes(), ["text", "text", "div#two", "text"]); }); /** * Example 2: ManBucketList * Let's up our ambitions a bit and make a bucket list for men: `ManBucketList`. * Being human, men's ambitions are driven by what they see around them every day. * By this logic, men's bucket lists should therefore also look different during weekdays and weekends. * During weekdays, men are stuck in long ques driving to work and fantasize about owning a red Ferrari. * During weekends, men stay at home, fiddle around the house and worry about their lawns, then drought * and then global warming. * * Therefore, on weekdays, the `ManBucketList` automatically adds an element "Buy Ferrari" to its list. * On weekends, the `ManBucketList` instead adds "Invent atmosphere decarbonizer". * * This time, things get a little more complex. * The elements added in the list of things to do before the man * dies now clearly originates from different origins. * The elements in our `BucketList` is *mixed* together from two different HTML documents: * both the shadowDOM and the lightDOM of our `<man-bucket-list>`. */ it("ManBucketList test", function () { class ManBucketList extends HTMLElement { constructor() { super(); const day = 1; //new Date().getDay(); const task = day < 5 ? "Buy Ferrari" : "Invent atmosphere decarbonizer"; this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = `<style id="sty">text-align: center;</style> <div id="man">${task}</div> <slot></slot>`; } } customElements.define("man-bucket-list", ManBucketList); const topDocument = document.createElement("div"); topDocument.innerHTML = ` <man-bucket-list id="manlist"> <div id="one">fix bike</div> <div id="two">slice cucumbers</div> </man-bucket-list> `; const list = topDocument.querySelector("#manlist"); const listShadow = list.shadowRoot; const listShadowSlot = listShadow.children[2]; testElementNodeListTagAndID(flattenNodes(list.childNodes), ["text", "div#one", "text", "div#two", "text"]); testElementNodeListTagAndID(listShadowSlot.assignedNodes(), ["text", "div#one", "text", "div#two", "text"]); testElementNodeListTagAndID(flattenNodes(listShadow.childNodes), ["style#sty", "text", "div#man", "text", "text", "div#one", "text", "div#two", "text"]); const one = topDocument.querySelector("#one"); list.removeChild(one); testElementNodeListTagAndID(flattenNodes(list.childNodes), ["text", "text", "div#two", "text"]); testElementNodeListTagAndID(listShadowSlot.assignedNodes(), ["text", "text", "div#two", "text"]); testElementNodeListTagAndID(flattenNodes(listShadow.childNodes), ["style#sty", "text", "div#man", "text", "text", "text", "div#two", "text"]); }); /** * Example 3: `MarriedManBucketList` * In this last example we will see how this list can evolve when * we add yet another document source for bucket list items: marriage. * * When men get married, their original ambitions and goals in life gets wrapped up in their marriage. * A good way to illustrate this is to keep the original `ManBucketList` and place that in the * shadowDOM of a new custom element: `MarriedManBucketList`. * * This example illustrate the problem of using one custom element * inside the shadowDom of another custom element. * (cf. Web components gold standard on content assignment). * Here, `flattenNodes(this.shadowRoot)` returns a much longer list: * `[style, div#man, div#love, div#romance, ..., div#one, div#two]`. * The bucket list items of `ManBucketList` are still there, * the only difference being that they now also include * a long list of goals intrinsic to `MarriedManBucketList`. * * What exactly is shadowDOM and lightDOM? * From the point-of-view of `ManBucketList`, the shadowDOM is still only `[style, div#man, slot]`. * But, what is `MarriedManBucketList`'s shadowDOM? * In the DOM, `ManBucketList`'s shadowDOM is organized *under* `MarriedManBucketList`'s shadowDOM. * It is a sub-document. So, are such sub-documents part of a shadowDOM? * * The answer is "no". * The sub-document's elements are not directly part of the `MarriedManBucketList` document. * Sub-documents cannot be directly styled or querySelected from the scope of a parent document in the DOM. * And, although possible, elements in sub-documents should not be directly queried nor manipulated from the scope of the parent document. * So even though the document of `ManBucketList` is wrapped in and subsumed under the shadowDOM of * `MarriedManBucketList`, the shadowDOM scope of `MarriedManBucketList` does not *reach into* that of `ManBucketList`. * If it does, then that is a hack and breach of contract. * * The lightDOM scopes are the reverse of the shadowDOM scopes. * From the point-of-view of `MarriedManBucketList` the lightDOM is simply *another document*. * For custom elements added to the main document, the lightDOM is always the main document. * But, the two `<slot>` elements in `MarriedManBucketList` and `ManBucketList` form a chained reference. * This chain transposes elements from *another document*, via `MarriedManBucketList` shadowDOM, and * down into `ManBucketList`. * Does that mean that the lightDOM of `ManBucketList` spans both * *another document* and the shadowDOM of `MarriedManBucketList`? * * Again, the answer is "no". * Even though actual elements can be transposed across three or more documents using `<slot>`s, * the term lightDOM is still only used about the document in which the `host` * of the custom element is instantiated. * In this instance, the lightDOM of `ManBucketList` is the shadowDOM of `MarriedManBucketList`. * This also illustrate that for custom elements used inside another custom elements, * the lightDOM is always the other shadowDom document and *never* the main document. * * However, references in `<slot>` elements *can* span across several documents. * And this means that the resolution of `assignedNodes()` and thus `visibleChildren` also span several documents. * The scope of `<slot>` and `visibleChildren` *does* include all the documents necessary. * So, sometimes an assignedNode of a slot can be found in the lightDOM's lightDOM. * Or an element is transposed to a shadowDOM's shadowDOM. */ it("MarriedManBucketList test", function (done) { class ManBucketList extends HTMLElement { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = ` <style id="sty">text-align: center;</style> <div id="man">Ferrari</div> <slot></slot> `; } } class MarriedManBucketList extends HTMLElement { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = ` <man-bucket-list-2 id="original"> <div id="a">love your wife</div> <div id="b">surprise her with a gift</div> <div id="c">make money</div> <div id="d">paint the house</div> <div id="e">fix her car</div> <div id="f">fix the plumbing</div> <div id="g">make more money</div> <div id="h">paint the fence, again</div> <div id="i">bite your tongue</div> <div id="j">paint the house in a different color</div> <slot></slot> </man-bucket-list-2> `; } } customElements.define("man-bucket-list-2", ManBucketList); Promise.resolve().then(() => { //there is a problem in the polyfill that the inner custom elements must be defined before the outer ones. customElements.define("married-man-bucket-list", MarriedManBucketList); const topDocument = document.createElement("div"); topDocument.innerHTML = ` <married-man-bucket-list id="marriedlist"> <div id="one">fix bike</div> <div id="two">slice cucumbers</div> </married-man-bucket-list> `; const marriedlist = topDocument.querySelector("#marriedlist"); const manlist = marriedlist.shadowRoot.children[0]; const marriedlistSlot = manlist.children[manlist.children.length - 1]; const manlistSlot = manlist.shadowRoot.children[2]; testElementNodeListTagAndID(flattenNodes(marriedlist.childNodes), ["text", "div#one", "text", "div#two", "text"]); testElementNodeListTagAndID(marriedlistSlot.assignedNodes(), ["text", "div#one", "text", "div#two", "text"]); testElementNodeListTagAndID(flattenNodes(manlist.childNodes), [ "text", "div#a", "text", "div#b", "text", "div#c", "text", "div#d", "text", "div#e", "text", "div#f", "text", "div#g", "text", "div#h", "text", "div#i", "text", "div#j", "text", "text", "div#one", "text", "div#two", "text", "text" ]); testElementNodeListTagAndID(manlistSlot.assignedNodes(), [ "text", "div#a", "text", "div#b", "text", "div#c", "text", "div#d", "text", "div#e", "text", "div#f", "text", "div#g", "text", "div#h", "text", "div#i", "text", "div#j", "text", "slot", "text" ]); const one = topDocument.querySelector("#one"); marriedlist.removeChild(one); testElementNodeListTagAndID(flattenNodes(marriedlist.childNodes), ["text", "text", "div#two", "text"]); testElementNodeListTagAndID(marriedlistSlot.assignedNodes(), ["text", "text", "div#two", "text"]); testElementNodeListTagAndID(flattenNodes(manlist.childNodes), [ "text", "div#a", "text", "div#b", "text", "div#c", "text", "div#d", "text", "div#e", "text", "div#f", "text", "div#g", "text", "div#h", "text", "div#i", "text", "div#j", "text", "text", "text", "div#two", "text", "text" ]); testElementNodeListTagAndID(manlistSlot.assignedNodes(), [ "text", "div#a", "text", "div#b", "text", "div#c", "text", "div#d", "text", "div#e", "text", "div#f", "text", "div#g", "text", "div#h", "text", "div#i", "text", "div#j", "text", "slot", "text" ]); done(); }); } ); });