joicomponents
Version:
Design patterns to build native web components
136 lines (122 loc) • 4.88 kB
JavaScript
/**
* Acknowledgments
*
* Many thanks to Jan Miksovsky and the Elix project for input and inspiration.
*/
function flattenNodes(nodes) {
let res = [];
for (let n of nodes) {
if (n.tagName === "SLOT") //if(node instanceof HTMLSlotElement) does not work in polyfill.
res = res.concat(n.assignedNodes({flatten: true}));
else
res.push(n);
}
return res;
}
function mapNodesByAttributeValue(nodes, attributeName) {
var res = {};
for (var i = 0; i < nodes.length; i++) {
var n = nodes[i];
var name = n.getAttribute ? (n.getAttribute(attributeName) || "") : "";
(res[name] || (res[name] = [])).push(n);
}
return res;
}
function arrayEquals(a, b) {
return b && a && a.length === b.length && a.every((v, i) => v === b[i]);
}
/**
* SlottableMixin adds a reactive lifecycle hook .slotchangedCallback(...) to its subclasses.
* This lifecycle hook is triggered every time a potentially assignable node for the element changes.
* .slotchangedCallback(...) triggers manually every time the element is attached to the DOM and
* whenever the a slotchange event would occur inside it.
* The callback does not require neither a `<slot>` nor a shadowRoot to be present on the custom element,
* it will trigger regardless.
*
* .slotchangedCallback(slotname, newAssignedNodes, oldAssignedNodes) is triggered every time:
* 1) the element is connected to the DOM,
* 2) whenever the slotted content of an element changes, but
* 3) except when the content of newAssignedNodes and oldAssignedNodes are equal.
*
* Gold standard: https://github.com/webcomponents/gold-standard/wiki/
* a) Detachment: SlottableMixin always starts observing when it is connected to the DOM and stops when it is disconnected.
* b) Content assignment: changes to assignedNodes of slotted children are notified as if the change happened to a normal child.
*
* @param Base class that extends HTMLElement
* @returns {SlottableMixin} class that extends HTMLElement
*
* todo when the childListChanges, I need a WeakMap that hold references to the previous values.
* todo This map is setup so that it is triggered created the first time the element is connected, but to the content
*/
function onlyOncePerMicroTaskCycle(register, key) {
if (register.has(key))
return false;
register.add(key);
Promise.resolve().then(() => {
register.delete(key);
});
return true;
}
const hostChildrenChanged = Symbol("hostChildrenChanged");
const hostSlotchange = Symbol("chainedSlotchangeEvent");
const init = Symbol("triggerAllSlotchangeCallbacks");
const slottables = Symbol("notFlatMap");
const microTaskRegister = Symbol("microTaskRegister");
class Slottables {
constructor(name, assigneds) {
this.name = name;
this.assigneds = assigneds;
}
assignedNodes(config) {
if (!(config && config.flatten === true))
return this.assigneds;
let res = [];
for (let n of this.assigneds) {
if (n.tagName === "SLOT") { //if(node instanceof HTMLSlotElement) does not work in polyfill.
const flat = n.assignedNodes({flatten: true});
res = res.concat(flat);
} else
res.push(n);
}
return res;
}
assignedElements(config) {
return this.assignedNodes(config).filter(n => n.nodeType === Node.ELEMENT_NODE);
}
}
export const SlotchangeMixinArnold = function (Base) {
return class SlotchangeMixinArnold extends Base {
constructor() {
super();
this[slottables] = {};
this[microTaskRegister] = new WeakSet();
requestAnimationFrame(() => this[init]());
}
[init]() {
const mo = new MutationObserver(() => this[hostChildrenChanged]());
mo.observe(this, {childList: true});
this.addEventListener("slotchange", e => this[hostSlotchange](e));
this[hostChildrenChanged]();
}
[hostSlotchange](e) {
const slot = e.composedPath().find(n => n.tagName === "SLOT" && n.parentNode === this);
if (!slot) //a slotchange event of a grandchild in the lightdom, not for me
return;
//todo stop propagation here??
if (!onlyOncePerMicroTaskCycle(this[microTaskRegister], slot))
return;
const slotName = slot.getAttribute("slot") || "";//todo test for this use of slotnames to guide the slot assigning
this.slotCallback(new Slottables(slotName, this[slottables][slotName]));
}
[hostChildrenChanged]() {
const children = mapNodesByAttributeValue(this.childNodes, "slot");
for (let name in children) {
if (arrayEquals(children[name], this[slottables][name]))
continue;
this[slottables][name] = children[name];
this.slotCallback(new Slottables(name, this[slottables][name]));
}
}
}
};
// [registered observers hold weak references/are automatically garbagecollected](https://dom.spec.whatwg.org/#garbage-collection)