joicomponents
Version:
Design patterns to build native web components
147 lines (119 loc) • 4.44 kB
Markdown
# Pattern: SlotchangeCallback
In [PostSlotchangeCallback](4_Pattern_PostSlotchangeCallback) we triggered an initial callback for
the web components using it that will be triggered once, immediately after a) `DOMContentLoaded` or
b) the first `slotchange` of an element would run (if it was created at startup).
In [SlotCallbackAfterDCL](3_Pattern_SlotCallbackAfterDCL) we queued and then triggered a `slotCallback`
using the relevant `<slot>` from the web component's own shadowDOM as argument.
In this pattern SlotchangeCallback, we are going to combine these two patterns.
This combination creates a `slotchangeCallback(slot)` that is triggered whenever a `slotchange` event
would be fired. The `slotchangeCallback(slot)` filters out SlotchangeNipSlips and delivers the
relevant `<slot>` element to the callback function.
However, in addition to doing this, the mixin that implements `slotchangeCallback(slot)` will also
fix the problem SlotchangeLostOnSafari. It does this by:
1. skipping all initial `slotchange` events.
These events occur in Chrome and Firefox, but not Safari.
2. manually locating all `<slot>` elements inside the shadowDOM,
3. trigger a `slotchangeCallback(slot)` for all of these `<slot>` elements that are not empty.
## Implementation: `SlotchangeMixin`
```javascript
const slotchangeListener = Symbol("slotchangeListener");
let constructedElements = [];
let isRunning = false;
function notNipSlip(composedPath, shadowRoot) {
for (let node of composedPath) {
if (node.tagName !== "SLOT")
return null;
if (node.getRootNode() === shadowRoot)
return node;
}
return null;
}
function doCallSlotCallback(slotchange) {
const slot = notNipSlip(slotchange, this.shadowRoot);
slot && this.slotCallback(slot);
}
function setupElements() {
for (let el of constructedElements) {
el.shadowRoot.addEventListener("slotchange", doCallSlotCallback.bind(el));
let slots = el.shadowRoot.querySelectorAll("slot");
for (let slot of slots) {
if (slot.assignedNodes().length > 0)
el.slotCallback(slot);
}
}
constructedElements = [];
isRunning = false;
}
if (document.readyState === "loading") {
isRunning = true;
document.addEventListener("DOMContentLoaded", function () {
setupElements();
});
}
function setupSlotchangeCallback(self) {
constructedElements.push(self);
if (isRunning)
return;
isRunning = true;
Promise.resolve().then(function () {
Promise.resolve().then(function () {
setupElements();
});
});
}
export function SlotchangeMixin(base) {
return class SlotchangeMixin extends base {
constructor() {
super();
setupSlotchangeCallback(this);
}
}
}
```
## Demo: SlotchangeCallback
```html
<template>
<style>
div { border: 4px solid green; }
</style>
<div>
<slot></slot>
</div>
</template>
<script type="module">
import {SlotchangeMixin} from "../../src/slot/SlotchangeMixin.js";
class GreenFrame extends SlotchangeMixin(HTMLElement) {
constructor() {
super();
this.attachShadow({mode: "open"});
const templ = document.querySelector("template").content.cloneNode(true);
this.shadowRoot.appendChild(templ);
}
slotchangeCallback(slot){
console.log(slot);
}
}
customElements.define("green-frame", GreenFrame);
document.addEventListener("DOMContentLoaded", function () {
const div = document.querySelector("div");
div.innerHTML = "<green-frame>¯\\_(ツ)_/¯</green-frame>";
});
</script>
<green-frame>¯\_(ツ)_/¯</green-frame>
<div>fill me up!</div>
```
## Discussion
The implementation `SlotchangeMixin` fixes several of the SlotMatroska problems:
1. SlotchangeLostOnSafari
2. PrematureSlotchange
3. SlotchangeNipSlip
4. SlotchangeSurprise
However, it still suffers from some problems. First, there is no callback when a `<slot>`
element is filled with fallback nodes initially. Second, FallbackNodesFallout is not fixed.
We need a mechanism to prevent an empty outer `<slot>` element marking an inner `<slot>`
element as filled. Third, the callback depends on an *inner* state change, something inside it
having changed. It would be better if the callback depended on an *outer* state change, something
outside it having changed that it could take advantage of inside. And fifth, it does not address
the problem of SlotStyleCreep. Can these problems be fixed?
## References
*