UNPKG

joicomponents

Version:

Design patterns to build native web components

647 lines (574 loc) 24.2 kB
import {StaticSlotchangeMixin} from "../../src/StaticSlotchangeMixin.js"; // const raf_x = (counter, cb) => requestAnimationFrame(counter === 1 ? cb : () => raf_x(counter - 1, cb)); function testElementNodeListTagAndID(nodes, ar) { let tagIds = nodes.map(n => { if (n.tagName) return n.tagName.toLowerCase() + (n.id ? "#" + n.id : ""); else return "text"; }); expect(tagIds).to.deep.equal(ar); } describe('StaticSlotchangeMixin basics', function () { var slotDIV; before(function () { slotDIV = document.createElement("div"); slotDIV.id = "slot"; document.querySelector("body").appendChild(slotDIV); }); it("instanceof HTMLElement + constructor() working + can be inputed to customElements.define()", function () { const SizeChangedElement = StaticSlotchangeMixin(HTMLElement); customElements.define("must-use-custom-elements-define-to-enable-constructor-slot", SizeChangedElement); const el = new SizeChangedElement(); expect(el.constructor.name).to.be.equal("StaticSlotchangeMixin"); assert(el instanceof HTMLElement); }); it("class extends StaticSlotchangeMixin(HTMLElement) + has method", function () { const SubclassSizeChangedElement = class SubclassSizeChanged extends StaticSlotchangeMixin(HTMLElement) { test() { return "abc"; } }; customElements.define("subclass-slot-changed", SubclassSizeChangedElement); const el = new SubclassSizeChangedElement(); expect(el.constructor.name).to.be.equal("SubclassSizeChanged"); assert(el instanceof HTMLElement); expect(el.test()).to.be.equal("abc"); }); //todo make mocha test for TypeError!! stupid mocha.. // it("class extends StaticSlotchangeMixin(HTMLElement) fails if it does not have shadowDOM when connected", function () { // const SubclassSizeChangedElement = class SubclassSizeChanged extends StaticSlotchangeMixin(HTMLElement) { // }; // customElements.define("slotchange-fails", SubclassSizeChangedElement); // const el = new SubclassSizeChangedElement(); // const b = slotDIV; // expect(()=> b.appendChild(el)).to.throw("wtf"); // }); it("subclass StaticSlotchangeMixin anonymous", function () { const SubclassSizeChangedElement = class extends StaticSlotchangeMixin(HTMLElement) { test() { return "abc"; } }; customElements.define("subclass-slot-changed-element", SubclassSizeChangedElement); const el = new SubclassSizeChangedElement(); expect(el.constructor.name).to.be.equal("SubclassSizeChangedElement"); assert(el instanceof HTMLElement); expect(el.test()).to.be.equal("abc"); }); it("imperatively trigger StaticSlotchangeMixin", function (done) { const Subclass = class Subclass extends StaticSlotchangeMixin(HTMLElement) { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = ` <div> <slot></slot> </div> `; } slotchangedCallback(slot, newFlattenedChildren, oldFlattenedChildren) { expect(slot).to.be.equal(""); // expect(slot).to.be.equal(this.shadowRoot.querySelector("slot")); expect(newFlattenedChildren[0].nodeName).to.be.equal("DIV"); expect(oldFlattenedChildren).to.be.equal(undefined); done(); } }; customElements.define("children-slot-div-added", Subclass); const el = new Subclass(); el.appendChild(document.createElement("div")); slotDIV.appendChild(el); //todo test if this breaks when we are testing and using polyfills // Promise.resolve().then(() => slotDIV.removeChild(el)); }); it("imperatively trigger StaticSlotchangeMixin, check that slots with no assigned value are considered empty", function (done) { const Subclass = class Subclass extends StaticSlotchangeMixin(HTMLElement) { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = ` <div> <slot></slot> </div> `; } slotchangedCallback(slot, newFlattenedChildren, oldFlattenedChildren) { expect(slot).to.be.equal(""); // expect(slot).to.be.equal(this.shadowRoot.querySelector("slot")); expect(oldFlattenedChildren).to.be.equal(undefined); expect(newFlattenedChildren.length).to.be.equal(1); expect(newFlattenedChildren[0].nodeName).to.be.equal("DIV"); done(); } }; customElements.define("slot-changed-div-and-slot-added", Subclass); const el = new Subclass(); el.appendChild(document.createElement("div")); el.appendChild(document.createElement("slot")); slotDIV.appendChild(el); //todo test if this breaks when we are testing and using polyfills // Promise.resolve().then(() => slotDIV.removeChild(el)); }); it("The super inner-outer-slot test", function (done) { var counter = 0; var counter2 = 0; var checks = 0; const InnerElementThatObserveChildren = class extends StaticSlotchangeMixin(HTMLElement) { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = ` <div><slot></slot></div> `; }; slotchangedCallback(slot, newFlattenedChildren, oldFlattenedChildren) { expect(slot).to.be.equal(""); // expect(slot).to.be.equal(this.shadowRoot.querySelector("slot")); console.log("inner", checks); if (counter === 0) { expect(oldFlattenedChildren).to.be.equal(undefined); expect(newFlattenedChildren.length).to.be.equal(2); expect(newFlattenedChildren[0].id).to.be.equal("a"); expect(newFlattenedChildren[1].id).to.be.equal("b"); counter++; } else if (counter === 1) { expect(oldFlattenedChildren.length).to.be.equal(2); expect(newFlattenedChildren.length).to.be.equal(3); expect(newFlattenedChildren[0].id).to.be.equal("a"); expect(newFlattenedChildren[1].id).to.be.equal("b"); expect(newFlattenedChildren[2].id).to.be.equal("c"); } if (++checks === 4) done(); } }; const OuterElementThatSlotsStuff = class extends StaticSlotchangeMixin(HTMLElement) { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = ` <inner-component-slot><slot></slot></inner-component-slot> `; } slotchangedCallback(slot, newFlattenedChildren, oldFlattenedChildren) { // expect(slot).to.be.equal(this.shadowRoot.querySelector("slot")); expect(slot).to.be.equal(""); console.log("outer", checks); if (counter2 === 0) { expect(oldFlattenedChildren).to.be.equal(undefined); expect(newFlattenedChildren.length).to.be.equal(2); expect(newFlattenedChildren[0].id).to.be.equal("a"); expect(newFlattenedChildren[1].id).to.be.equal("b"); counter2++; } else if (counter2 === 1) { expect(oldFlattenedChildren.length).to.be.equal(2); expect(newFlattenedChildren.length).to.be.equal(3); expect(newFlattenedChildren[0].id).to.be.equal("a"); expect(newFlattenedChildren[1].id).to.be.equal("b"); expect(newFlattenedChildren[2].id).to.be.equal("c"); } if (++checks === 4) done(); } }; customElements.define("inner-component-slot", InnerElementThatObserveChildren); customElements.define("outer-component-slot", OuterElementThatSlotsStuff); const el = document.createElement("outer-component-slot"); //no slotchange event until el is connected to the DOM el.innerHTML = `<div id="a">a</div><div id="b">b</div>`; slotDIV.appendChild(el); let c = document.createElement("div"); c.id = "c"; el.appendChild(c); // Promise.resolve().then(() => slotDIV.removeChild(el)); }); it("not listening for slotChange on slots that are not a direct child", function (done) { const InnerElementThatObserveChildren = class extends StaticSlotchangeMixin(HTMLElement) { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = `<slot></slot>`; }; slotchangedCallback(slot, newFlattenedChildren, oldFlattenedChildren) { expect(slot).to.be.equal(""); // expect(slot).to.be.equal(this.shadowRoot.querySelector("slot")); expect(oldFlattenedChildren).to.be.equal(undefined); expect(newFlattenedChildren.length).to.be.equal(3); expect(newFlattenedChildren[0].nodeType).to.be.equal(3); expect(newFlattenedChildren[1].nodeName).to.be.equal("DIV"); expect(newFlattenedChildren[2].nodeType).to.be.equal(3); done(); } }; const OuterElementThatSlotsStuff = class extends HTMLElement { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = ` <inner-listener-slot> <div> <slot></slot> </div> </inner-listener-slot>`; } }; customElements.define("inner-listener-slot", InnerElementThatObserveChildren); customElements.define("outer-with-grandchild-slot-2", OuterElementThatSlotsStuff); const el = new OuterElementThatSlotsStuff(); slotDIV.appendChild(el); //slotchangedCallback should be triggered here, because it is triggered at connectedCallback.. el.appendChild(document.createElement("p")); //but slotchangedCallback should not be triggered here, since "p" is not a slotable node for inner-listener-slot // Promise.resolve().then(() => slotDIV.removeChild(el)); }); it("Event slotchange", function (done) { let counter = 0; const InnerElementIsSlot = class extends StaticSlotchangeMixin(HTMLElement) { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = `<slot></slot>`; }; slotchangedCallback(slot, newFlattenedChildren, oldFlattenedChildren) { expect(slot).to.be.equal(""); // expect(slot).to.be.equal(this.shadowRoot.querySelector("slot")); if (counter === 0) { expect(oldFlattenedChildren).to.be.equal(undefined); expect(newFlattenedChildren.length).to.be.equal(0); counter++; return; } if (counter === 1) { expect(oldFlattenedChildren).to.deep.equal([]); expect(newFlattenedChildren.length).to.be.equal(1); expect(newFlattenedChildren[0].nodeName).to.be.equal("P"); done(); } } }; const OuterElementIsSlot = class extends HTMLElement { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = ` <inner-slot><slot></slot></inner-slot>`; } }; customElements.define("inner-slot", InnerElementIsSlot); customElements.define("outer-slot", OuterElementIsSlot); const el = new OuterElementIsSlot(); slotDIV.appendChild(el); Promise.resolve().then(() => { el.appendChild(document.createElement("p")); // Promise.resolve().then(() => { // slotDIV.removeChild(el); // }); }); }); it("slotchangedCallback triggered on connect and re-connect, but NOT when the node is NOT connected to the DOM", function (done) { let counter = 0; const Subclass = class Subclass extends StaticSlotchangeMixin(HTMLElement) { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = `<slot></slot>`; }; slotchangedCallback(slot, newFlattenedChildren, oldFlattenedChildren) { expect(slot).to.be.equal(""); // expect(slot).to.be.equal(this.shadowRoot.querySelector("slot")); if (counter === 0) { expect(oldFlattenedChildren).to.be.equal(undefined); expect(newFlattenedChildren.length).to.be.equal(1); expect(newFlattenedChildren[0].nodeName).to.be.equal("DIV"); counter++; } else if (counter === 1) { expect(newFlattenedChildren.length).to.be.equal(3); expect(newFlattenedChildren[0].nodeName).to.be.equal("DIV"); expect(newFlattenedChildren[1].nodeName).to.be.equal("DIV"); expect(newFlattenedChildren[2].nodeName).to.be.equal("DIV"); done(); } } }; customElements.define("connected-disconnected-connected-slot", Subclass); const el = new Subclass(); el.appendChild(document.createElement("div")); //is not triggered. slotDIV.appendChild(el); //slotchangedCallback triggered on connect slotDIV.removeChild(el); //disconnect el.appendChild(document.createElement("div")); //is not triggered. el.appendChild(document.createElement("div")); //is not triggered. slotDIV.appendChild(el); //slotchangedCallback triggered on connect Promise.resolve().then(() => slotDIV.removeChild(el)); }); it("slotchangedCallback triggered on connect and re-connect with wait.", function (done) { let counter = 0; const Subclass = class Subclass extends StaticSlotchangeMixin(HTMLElement) { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = `<slot></slot>`; }; slotchangedCallback(slot, newFlattenedChildren, oldFlattenedChildren) { expect(slot).to.be.equal(""); // expect(slot).to.be.equal(this.shadowRoot.querySelector("slot")); if (counter === 0) { expect(oldFlattenedChildren).to.be.equal(undefined); expect(newFlattenedChildren.length).to.be.equal(1); expect(newFlattenedChildren[0].nodeName).to.be.equal("DIV"); counter++; return; } if (counter === 1) { expect(oldFlattenedChildren.length).to.be.equal(1); expect(newFlattenedChildren.length).to.be.equal(3); expect(newFlattenedChildren[0].nodeName).to.be.equal("DIV"); expect(newFlattenedChildren[1].nodeName).to.be.equal("DIV"); expect(newFlattenedChildren[2].nodeName).to.be.equal("DIV"); done(); } } }; customElements.define("connected-settimeout-disconnected-connected-slot", Subclass); const el = new Subclass(); el.appendChild(document.createElement("div")); //is not triggered. slotDIV.appendChild(el); //slotchangedCallback triggered on connect Promise.resolve().then(() => slotDIV.removeChild(el)); //disconnect setTimeout(() => { el.appendChild(document.createElement("div")); //is not triggered. el.appendChild(document.createElement("div")); //is not triggered. slotDIV.appendChild(el); //slotchangedCallback triggered on re-connect Promise.resolve().then(() => slotDIV.removeChild(el)); //disconnect }, 50); }); it("slotchangedCallback is not triggered when grandchildren of host changes.", function (done) { let counter = 0; const Subclass = class Subclass extends StaticSlotchangeMixin(HTMLElement) { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = `<slot></slot>`; }; slotchangedCallback(slot, newFlattenedChildren, oldFlattenedChildren) { expect(slot).to.be.equal(""); // expect(slot).to.be.equal(this.shadowRoot.querySelector("slot")); if (counter === 0) { expect(oldFlattenedChildren).to.be.equal(undefined); expect(newFlattenedChildren.length).to.be.equal(1); expect(newFlattenedChildren[0].id).to.be.equal("one"); counter++; } else if (counter === 1) { expect(oldFlattenedChildren.length).to.be.equal(1); expect(newFlattenedChildren.length).to.be.equal(2); done(); } } }; customElements.define("grandchildren-not-slotchange", Subclass); const el = new Subclass(); el.innerHTML = "<div id='one'></div>"; slotDIV.appendChild(el); //slotchangedCallback triggered on connect el.querySelector("#one").appendChild(document.createElement("p")); //should not trigger slotchange as it is a grandchild of host el.appendChild(document.createElement("span")); //should trigger slotchange as it is a child of host }); it("slotchangedCallback is not triggered when grandchildren with relevant slot='name' of host changes.", function (done) { let counter = 0; const Subclass = class Subclass extends StaticSlotchangeMixin(HTMLElement) { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = `<slot name="abc"></slot>`; }; slotchangedCallback(slot, newFlattenedChildren, oldFlattenedChildren) { expect(slot).to.be.equal("abc"); // expect(slot).to.be.equal(this.shadowRoot.querySelector("slot")); if (counter === 0) { expect(oldFlattenedChildren).to.be.equal(undefined); expect(newFlattenedChildren.length).to.be.equal(0); // expect(newFlattenedChildren[0].id).to.be.equal("one"); counter++; } else if (counter === 1) { expect(oldFlattenedChildren.length).to.be.equal(0); expect(newFlattenedChildren.length).to.be.equal(1); expect(newFlattenedChildren[0].nodeName).to.be.equal("P"); done(); } } }; customElements.define("grandchildren-named-not-slotchange", Subclass); const el = new Subclass(); el.innerHTML = "<div id='one'></div>"; slotDIV.appendChild(el); //slotchangedCallback triggered on connect let newChild = document.createElement("p"); newChild.setAttribute("slot", "abc"); el.querySelector("#one").appendChild(newChild); //should not trigger slotchange as it is a grandchild of host el.querySelector("#one").removeChild(newChild); el.appendChild(newChild); //should trigger slotchange as it is a child of host }); it("no name slot does not slot a named host's child element", function (done) { const Subclass = class Subclass extends StaticSlotchangeMixin(HTMLElement) { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = `<slot></slot>`; }; slotchangedCallback(slot, newFlattenedChildren, oldFlattenedChildren) { expect(slot).to.be.equal(""); // expect(slot).to.be.equal(this.shadowRoot.querySelector("slot")); expect(oldFlattenedChildren).to.be.equal(undefined); expect(newFlattenedChildren.length).to.be.equal(1); expect(newFlattenedChildren[0].id).to.be.equal("aaa"); done(); } }; customElements.define("named-slot-not-into-no-named-slot", Subclass); const el = new Subclass(); el.innerHTML = "<div id='aaa'></div>"; slotDIV.appendChild(el); //slotchangedCallback triggered on connect let newChild = document.createElement("p"); newChild.setAttribute("slot", "abc"); el.querySelector("#aaa").appendChild(newChild); //should not trigger slotchange as it is a grandchild of host el.querySelector("#aaa").removeChild(newChild); el.appendChild(newChild); //should not trigger slotchange as name and slot differs el.removeChild(newChild); }); it("filter slot on id", function (done) { var phase = 0; const Subclass = class Subclass extends StaticSlotchangeMixin(HTMLElement) { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = `<slot id="one"></slot><slot name="abc"></slot>`; }; slotchangedCallback(slot, newFlattenedChildren, oldFlattenedChildren) { if (phase === 0){ if (slot === ""){ // if (slot.id === "one"){ expect(newFlattenedChildren[0].id).to.be.equal("aaa"); } else { expect(slot).to.be.equal("abc"); // expect(slot.getAttribute("name")).to.be.equal("abc"); expect(newFlattenedChildren.length).to.be.equal(0); } } else if (phase === 1){ expect(slot).to.be.equal("abc"); // expect(slot.getAttribute("name")).to.be.equal("abc"); done(); } } }; customElements.define("fliter-slot-id", Subclass); const el = new Subclass(); el.innerHTML = "<div id='aaa'></div>"; slotDIV.appendChild(el); //2x slotchangedCallback triggered on connect phase = 1; let newChild = document.createElement("p"); newChild.setAttribute("slot", "abc"); el.appendChild(newChild); //only 1x slotchange is triggered }); //todo does `<slot name="xyz" slot="abc">` work in Safari and the polyfill? it("blue-frame.", function (done) { var counter = 0; class BlueFrame extends StaticSlotchangeMixin(HTMLElement) { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = `<style> :host { display: inline-block; border: 10px solid blue; } #sold { display: none; background: red; border-radius: 50%; width: 10px; height: 10px; position: absolute; bottom: 5px; right: 5px; } :host([sold]) #sold { display: block; } </style> <passe-partout-2> <slot name="label" slot="label"></slot> <slot></slot> <div id="sold"></div> </passe-partout-2> `; } slotchangedCallback(slot, newNodes, oldNodes) { counter++; // slot = slot.name; if (slot === "") { expect(oldNodes).to.be.equal(undefined); testElementNodeListTagAndID(newNodes, ["img"]); } else if (slot === "label") { expect(oldNodes).to.be.equal(undefined); testElementNodeListTagAndID(newNodes, ["span"]); } else { assert(false); } if (counter === 4) done(); } } class PassePartout extends StaticSlotchangeMixin(HTMLElement) { constructor() { super(); this.attachShadow({mode: "open"}); this.shadowRoot.innerHTML = `<style> :host { display: inline-block; position: relative; background: white; padding: 12px; } div { text-align: center; } </style> <slot></slot> <div id="label"> <slot name="label"></slot> </div> `; } slotchangedCallback(slot, newNodes, oldNodes) { counter++; // slot = slot.name; if (slot === "") { expect(oldNodes).to.be.equal(undefined); testElementNodeListTagAndID(newNodes, ["text", "text", "img", "text", "div#sold", "text"]); } else if (slot === "label") { expect(oldNodes).to.be.equal(undefined); testElementNodeListTagAndID(newNodes, ["span"]); } else { assert(false); } if (counter === 4) done(); } } customElements.define("passe-partout-2", PassePartout); customElements.define("blue-frame-2", BlueFrame); /* <blue-frame sold> <img> <span slot="label">Picture of the ocean</span> </blue-frame> */ const el = new BlueFrame(); el.appendChild(document.createElement("img")); //is not triggered. let span = document.createElement("span"); span.setAttribute("slot", "label"); span.innerText = "Picture of the ocean"; el.appendChild(span); //is not triggered. document.querySelector("body").appendChild(el); //slotchangedCallback triggered on connect }); });