[WICG/webcomponents] making a SlotChangeObserver similar to MutationObserver childList (Issue #1042)

# The issue

The `slotchange` event is similar to DOM Mutation Events (but better), but it does not provide a list of changes (being better, it is batched, , so someone using this event to track which slotted nodes were added and which were removed has to implement a diff strategy by tracking previous slotted nodes to diff against.

But a diff strategy ends up in net-zero changes if an element was disconnected from DOM and then reconnected to the same parent ***in the same tick***, because the end result is that the reconnected node is still slotted to the same slot. Because `slotchange` runs once in a future task after the current macrotask, all changes are lumped into one event with no way to detect what happened apart from diffing.

Here is code showing what we want to do (but which is not currently possible):

```js
let slot = this.shadowRoot.querySelector(".some-slot");

slot.addEventListener("slotchange", (e) => {
  // imagine something similar to MutationObserver:
  for (const change of event.records) {
    for (const node of change.addedNodes) runLogicForAddedNode(node)
    for (const node of change.removedNodes) runLogicForRemovedNode(node)
  }
});
```

With `MutationObserver`, it is possible to react to each mutation. When we are building complex apps that rely on the composed tree shape, it is desirable to be able to react to all mutations. Currently:

- connected/disconnectedCallbacks run faithfully each time
- MutationObserver childList changes allow running a reaction faithfully for all mutations
- slotchange does not allow logic per mutation

Because of this discrepancy, complex state that relies on dis/connectedCallback, MutationObserver, and slotchange together, can get out of sync when a synchronous disconnect and reconnect happens:

- disconnectedCallback and connectedCallback will fire (destroy stuff, re-create stuff)
- MutationObserver records for disconnect and connect will be iterable (destroy stuff, recreate stuff)
- nothing happens with slotchange because the diff shows no changes, something that may have been dependent on both connectedCallback and slot distribution is out of sync.

# Possible solution

Either `slotchange` event objects can be updated to include change records, or a new `SlotChangeObserver` (or similar named API) can be added with very similar shape to MutationObserver but only for slot change added/removed nodes.

> [!Important]
> If we add change records to the `slotchange` events, or a new `SlotChangeObserver` API, we must be absolutely sure to not repeat the problem that `MutationObserver` has, so sanity sake:
> 
> - https://github.com/whatwg/dom/issues/1111

Besides the above hypotehtical example using `slotchange` events, here's a hypothetical `SlotChangeObserver` example:

```js
let slot = this.shadowRoot.querySelector(".some-slot");

const observer = new SlotChangeObserver((records) => {
  // very similar to MutationObserver
  for (const change of records) {
    for (const node of change.addedNodes) runLogicForAddedNode(node)
    for (const node of change.removedNodes) runLogicForRemovedNode(node)
  }
})

observer.observe(slot, {flattened: true /* or false, as with slot.assignedNodes */})
```

# Current workaround

The first workaround I can imagine is: on each `slotchange` event, run "removed" logic for all previous slotted nodes (whether or not they are still slotted), then run "added" logic for all current slotted nodes.

If there are a lot of slotted nodes and only a few were slotted/unslotted, there will be a lot of unnecessary work done for all the other nodes, because it is not possible to determine which ones were added/removed in the same tick.

-- 
Reply to this email directly or view it on GitHub:
https://github.com/WICG/webcomponents/issues/1042
You are receiving this because you are subscribed to this thread.

Message ID: <WICG/webcomponents/issues/1042@github.com>

Received on Friday, 22 December 2023 08:30:30 UTC