Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Observing slotchange mutation records: a SlotChangeObserver API similar to MutationObserver with childList:true #1042

Open
trusktr opened this issue Dec 22, 2023 · 3 comments

Comments

@trusktr
Copy link

trusktr commented Dec 22, 2023

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):

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:

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

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 */})
@trusktr trusktr changed the title making a SlotChangeObserver similar to MutationObserver childList Observing slotchange mutation records: a SlotChangeObserver API similar to MutationObserver with childList:true Dec 22, 2023
@smaug----
Copy link

slotchange doesn't use a separate task
https://dom.spec.whatwg.org/#notify-mutation-observers

@Westbrook
Copy link

Definitely in agreement that we need more power in this area: #933 The other thing that slotchange (or a possible SlotChangeObserver) provides over MutationObserver is the ability to flatten: true the results of the change.

@michaelwarren1106
Copy link

+1 to this idea.

Another thing that I'd like to advocate for with slotchange enhancements is that a slot change event also trigger for direct child slotted element attributes and text node changes. I know that detecting attribute/property/text node changes is a slippery slope because of depth and performance issues.

That said, if the slotchange would fire when existing slotted DOM elements attributes/textContent change, that would be a huge help to web component developers.

Rationale

A lot (all?) front end template frameworks will try to intelligently re-use DOM elements when state updates cause DOM re-renders. I know for a fact that both React and Lit intelligently re-use DOM when possible. If slotted children of a web component are re-used by a framework re-render cycle and thereby only the attributes/textContent are changed, slotchange won't fire and the component depending on slotchange won't behave accordingly.

Currently developers that consume web components implemented wth slotchange have to jump through hoops to make sure that DOM isn't re-used so that slotchange can fire successfully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants