joicomponents
Version:
Design patterns to build native web components
159 lines (142 loc) • 7.07 kB
Markdown
## how to fix slotchange
2. The initial-`slotchange`-event is not always dispatched in Safari (upto current version oct 2018).
//todo The tests that I have that shows the slotchange events in different browsers illustrated exactly
where this problem was
The initial-`slotchange`-event is the `slotchange` event that is dispatched
when the slot is created and assigned nodes for the very first time.
Both Chrome and the polyfill dispatch an initial-`slotchange`-event, but Safari does not.
This discrepancy can be harmonized in the following ways:
1. Ignore the initial-`slotchange`-event, except in Safari.
2. Create an extra `slotchange`-event at `connectedCallback()` only in Safari.
3. Create an extra `slotchange`-event in all browser, but
ensure that the `slotchange`-event listener is only run when the `.assignedNodes()`
of the `<slot>` has actually changed.
## Example 1: RedFrame
In this example we create a `<red-frame>`.
The `<red-frame>` will present its content inside a red frame, but
it also displays the number of objects inside its frame in the top left corner.
```html
<script>
function compareArrays(a, b) { //[1]
return a && b && a.length === b.length && a.every((v, i) => v === b[i]);
}
class RedFrame extends HTMLElement {
constructor(){
super();
this.previouslyAssignedNodes = undefined; //[2]
this.attachShadow({mode: "open"});
this.shadowRoot.innerHTML = //[3]
`<style>
:host {
display: inline-block;
border: 10px solid red;
}
</style>
<div id="count"></div>
<slot></slot>`;
}
connectedCallback(){ //[4]
const slot = this.shadowRoot.querySelector("slot");
slot.addEventListener("slotchange", this.assignedNodesChanged.bind(this));
this.assignedNodesChanged(); //[5]
}
disconnectedCallback(){ //[8]
const slot = this.shadowRoot.querySelector("slot");
slot.removeEventListener("slotchange", this.assignedNodesChanged.bind(this));
}
assignedNodesChanged(){ //[6]
const slot = this.shadowRoot.querySelector("slot");
const currentlyAssignedNodes = slot.assignedNodes();
if (!compareArrays(this.previouslyAssignedNodes, currentlyAssignedNodes)){
this.previouslyAssignedNodes = currentlyAssignedNodes;
this.updateCount(this.previouslyAssignedNodes.length);
}
}
updateCount(count){ //[7]
const div = this.shadowRoot.getElementById("count");
div.innerText = this.cachedCount;
}
}
customElements.define("red-frame", RedFrame);
</script>
<red-frame> <!--[9]-->
<img src="tomato.jpg" alt="tomato">
<img src="strawberry.jpg" alt="strawberry">
</red-frame>
<script>
setTimeout(function(){
const cherry = document.createElement("img");
cherry.src = "cherry.jpg";
cherry.alt = "cherry";
document.querySelector("red-frame").appendChild(cherry); //[10]
}, 2000);
</script>
```
1. A function `compareArrays(a,b)` is added to be used later.
2. A property `.previouslyAssignedNodes` is added to `<red-frame>` element to
cache the previous values of the slotted nodes.
3. The `<red-frame>` is set up with a `<div>` to display the count
and a `<slot>` to display the content.
4. When the `<red-frame>` is connected to the DOM,
the `.assignedNodesChanged()` function is added as an event listener
directly on the `<slot>` node. It is *not* possible to add the event listener on
`this.shadowRoot`, as the `slotchange` event *does not bubble*.
5. At the end of `connectedCallback()` an extra call to the `slotchange`
event listener function `.assignedNodesChanged()` is added manually.
This will trigger an equivalent initial-`slotchange`-event in Safari.
6. When `.assignedNodesChanged()` is triggered,
it checks to see if the currently assigned nodes have changed since
it last was triggered. This ensures that the manually added
initial-`slotchange`-event does not trigger twice in Chrome and the polyfill.
7. Finally, `updateCount(count)` is triggered when the `.assignedNodes()`
actually have changed, and updates the text inn the `<div id="count">`.
8. Whenever the element is disconnected from the DOM,
the `slotchange` event listener is cleaned up.
9. When the example is first loaded,
a `<red-frame>` element with two `<img>` is connected to the DOM.
The `.connectedCallback()` of `<red-frame>` trigger `.assignedNodesChanged()` manually
in all browsers and causes an additional `slotchange` event in Chrome and the polyfill.
10. After 2 sec a third image is added. This causes a `slotchange` event in all browsers.
Adding the `slotchange` listener, caching the assigned nodes, and checking the assigned nodes
to cancel redundant callbacks is adding code and complexity to our web components.
To encapsulate this complexity we can create a Mixin that we can reuse across elements.
There are two ways to do so, and you can read more about them in
[SlotchangeMixin](../../trash/book/chapter3_element_lifecycle/chapter3_lifecycle/chapter3_old/Mixin1_SlotchangeMixin.md).
## Example: IfOnly
A good way to understand the principle behind `slotchange` and the `.slotchangedCallback()`,
is to look at an example of how `<red-frame>` would have been implemented **if only**
Safari had thrown the initial-`slotchange`-event *and* both Safari and Chrome had made
`slotchange` bubble.
```javascript
class IfOnly__RedFrame extends HTMLElement {
constructor(){
super();
this._slotChangedListener = (e) => this.slotchangedCallback(e);
this.attachShadow({mode: "open"});
this.shadowRoot.innerHTML =
`<style>
:host {
display: inline-block;
border: 10px solid red;
}
</style>
<div id="count"></div>
<slot></slot>`;
this.count = this.shadowRoot.getElementById("count");
}
connectedCallback(){
this.shadowRoot.addEventListener("slotchange", this._slotChangedListener);
}
disconnectedCallback(){
this.shadowRoot.removeEventListener("slotchange", this._slotChangedListener);
}
slotchangedCallback(e){
this.count.innerText = e.currentTarget.assignedNodes().length;
}
}
customElements.define("red-frame", IfOnly__RedFrame);
```
In this example, there is no need to cache the previously assigned nodes nor
to discover nor update the `<slot>` elements inside `this.shadowRoot`.
This *greatly* reduces the complexity of listening for `slotchange` events and
thus the complexity of the components that do so.