WICG / webcomponents

Web Components specifications
Other
4.39k stars 375 forks source link

Need "slotchange" event #288

Closed dglazkov closed 8 years ago

dglazkov commented 9 years ago

For developers, it will be good to have an event that is fired whenever slotting algorithm runs within a shadow tree. Maybe runs and produces changes compared to the previous result? Don't know. Also don't know if this is v1 or v2.

annevk commented 8 years ago

The fallback case means remove needs to run "slotchange(parent)" when a node is removed from a parent that is a slot with assigned nodes being empty.

hayatoito commented 8 years ago

Yes, that's exactly same to what I am about to say for "remove part"

Remove:
  Node has a parent slot whose assigned nodes are empty
    -> slotchange(parent)
annevk commented 8 years ago

By the way, let slot2 = findSlot(slot): in your algorithm earlier on for "slotchange" can just be let slot2 be slot's assigned slot. No need to run findSlot().

annevk commented 8 years ago

I posted a PR at https://github.com/whatwg/dom/pull/229 that I'd appreciate feedback on. I think there's some potential for more cleanup to what I did and I'm going to study it more now, but it should meet all requirements as-is.

trusktr commented 7 years ago

As an end-user, having a slotchange event feels against the grain of MutationObserver movement. Using MutationObserver would follow new and recommended patterns (not considering performance).

With the current slotchange event, I am running into the automatic requirement that I must now calculate myself which nodes were added or removed (which may be O(n)), but, otherwise, why on earth would I even care about the slotchange event? Why not use MutationObserver with an option to opt-in to distribution changes just like childList? Performance of an opt-in feature is O(1) when not opting-in (a conditional check).

@rniwa said,

I think the use case matters here. It looks like the all use cases listed here are about updating the UI / rendering to accommodate changes in the distributed nodes, and not so much about updating DOM / API surface for component users. So it's actually desirable for those updates to not take place until the end of the current task.

I do that myself by deferring my render logic to an animation frame. But, not all users may be adept enough to do that, and those users may not necessarily care about animation anyways. In fact, those users probably will never touch MutationObserver.

@annevk said,

If you do your own bookkeeping, you can replay. This is similar to attribute records without old values. But instead here we don't even provide a way to get to the old value (you have to store it yourself) and new value you have to get yourself too through a method.

What are the use cases where people want to listen to slotchange but don't want to see what the changes were in the distributed nodes? In my case (coincidentally?) I must detect which nodes were distributed or undistributed, otherwise slotchange isn't useful for me. For perspective, my lib is tracking the flat tree in order to render it to WebGL, so knowing which nodes are assigned/distributed is required.

@rniwa said,

With slotchange, only thing we can observe is that the list of distributed nodes have changed. It doesn't tell us which nodes are added or removed into/from where.

Because of this, I'm opposed to using mutation observers unless the list of nodes that got inserted and removed are also provided.

Yes, please. I myself at least don't have any other reason to listen to slotchange other than to get the list of nodes that got inserted and removed. Is it coincidence that the first time I need to use slotchange I also need to calculate which nodes were added/removed?

But @hayatoito and I both agree that doing so would be prohibitively expensive in both Blink and WebKit so I don't think this is an option.

If we use MutationObserver, with an opt-in option, how is that prohibitively more expensive than having to do it manually in JavaScript? It's not expensive at all if users don't opt-in similarly to childList, O(1).

@treshugart said,

We haven't come up with a use case warranting knowing the exact changes yet for a slot change, but we also haven't really used the event in anger yet.

For my lib to be able to keep track of the final flat tree (for rendering in WebGL), this is needed. When nodes are assigned into slots, they have new parents with respect to the WebGL render tree (i.e. they are transformed relative to their slot parent). My lib keeps track of two things: when nodes are "possibly distributed" in which case they are not rendered relative to their original parents, and when those same nodes are "assigned", in which case they are rendered relative to their slot parents. So, when adding a shadow root to a host, the host's children automatically become "possibly distributed" and will not be rendered unless they happen to be distributed to a slot, and the rendering happens relative to that new location.

For more perspective, I believe that if a library like http://aframe.io (which renders Custom HTML Elements to WebGL via Three.js) wants to be compatible with ShadowDOM, then that library will also have to know how nodes are distributed (i.e. keep track of the flat tree), and the most obvious way to do that is keeping track of nodes that are assigned/unassigned to slots. Using MutationObserver for this would be much cleaner than making us do it in user-land.

The problem is, we need some way to determine the final position of a distributed node. For example, if a slot S1 is distributed to a slot S2, which is distributed to S3, which is distributed to S4, then a node assigned to S1 would be finally distributed to S4 and would render relative to the parent of S4. This is what want to keep track of so that I can render to WebGL. If all the shadow trees are open, then this is no problem: I can check slot.assignedSlot.assignedSlot.assignedSlot.etc in a loop until I get null, in which case I'll be able to tell the final assignment of a node. But some trees are closed. Hijacking attachShadow can help work around the closed trees.

Besides slot.assignedNode(), what if there was something like slot.distributedNodes() to find the final distribution? It would return an array that contains only nodes that are rendered relative to that slot. In the above example, S1, S2, and S3 would all return empty arrays, while S4 would return an array with nodes which are rendered relative to that slot's parent.

insert

  The node is being inserted into a parent node that also has a shadow root
    -> let slot be findSlot(node)
    -> if slot is non-null:
      -> run assignSlotables(slot) // this is naive, but good enough for spec
      -> slotchange(slot)

Does that slotchange(slot) happen before the connected/disconnected events/callbacks of the node that is being inserted into the parent node? I think that since distribution is a less-likely event than connected/disconnected (i.e. all nodes have connected/disconnected events, but not all nodes are distributed), then it would be convenient for the slotchange event to fire first, which would make it easier to prevent some otherwise-normal connection/disconnection logic when the condition of distribution is detected, rather than canceling or reversing that logic after distribution is detected and it happens that both happened in the same tick synchronously.

trusktr commented 7 years ago

Revised my previous comment.

I have a clearer picture after some testing. slot.assignedNodes() basically gives the literally assigned nodes on the context slot, while slot.assignedNodes({flatten: true}) gives the distributed nodes at the context slot. However, slot.assignedNodes({flatten: true}) returns nodes even if the nodes have been further distributed to a deeper node. This is similar to my previous slot.distributedNodes() idea, except that it doesn't return an empty array for the intermediate slots.

It might be nice to have a method (f.e. slot.distributedNodes() or slot.assignedNodes({distributed: true})) that when called on the slots S1 through S4 of my example will return distributed nodes only when called on S4. Does that make sense?

For example, here's a jsfiddle showing S1 through S4 assigned nodes flattened: https://jsfiddle.net/trusktr/9bs9mryt/

The output looks like this:

slot change on s1: [p]
slot change on s2: [p]
slot change on s3: [p]
slot change on s4: [p]

If we had a new method for getting strictly finally distributed nodes (slot.distributedNodes() or slot.assignedNodes({distributed: true})), the output would look like this:

slot change on s1: []
slot change on s2: []
slot change on s3: []
slot change on s4: [p]

where we see the final position of the <p> tag is at S4. If we were to unassign S3 from S4, then the output would be:

slot change on s1: []
slot change on s2: []
slot change on s3: [p]
slot change on s4: []

Get what I mean? I think that I believe that that would be useful for my case where I want to know the final positions where nodes are distributed.

I believe that I can currently achieve this by hijacking attachShadow so that I can always have shadow root references and by extension always have slot references, and then hack some way to find final distributions during slotchange events. Would love ideas on if there's some clean way to do it.

Made a new issue for the idea: https://github.com/w3c/webcomponents/issues/611