Closed surma closed 7 years ago
Why is it not precise?
@annevk Maybe I am not understanding the spec, that is entirely possible. If it is specced, do you mind telling me which browser is behaving correctly so I can file bugs against the others?
However, maybe we can have a discussion if it is possible/feasible to change the spec to the behavior I describe below or help me understand why that is not a good idea™️
As far as I can tell the specification consistently fires slotchange however the tree was created. Meaning only "ShadyDOM" is correct (though I haven't looked at the timing). If your understanding is different you'll have to tell me how you arrived at that conclusion.
I think the following chromium bug is related: https://bugs.chromium.org/p/chromium/issues/detail?id=696659
I believe that the current Blink's behavior satisfies spec-conformance. As I mentioned in the bug, I guess what is being proposed here is:
From https://github.com/whatwg/dom/pull/229#issuecomment-211734744
When inserting a slot, we set a suppress signaling flag.
If we change it to:
When inserting a slot, we do not set a suppress signaling flag.
Then, I think the spec's behavior matches that of Shady DOM.
See https://github.com/w3c/webcomponents/issues/288#issuecomment-206791125 by @rniwa for why we suppress in that case. I'm okay with changing this if you're all agreed (including @rniwa) we should change this.
Anyone interested in writing tests?
Just want to reemphasize that I am arguing from a developer experience perspective here, and that the current state is unexpected (and inconvenient).
I’d actually be happy to take this opportunity learn to write tests and help out that way. I’m sure one of my colleagues could help me get started here. (@domenic?)
I appreciate you filing the issue. I just wasn't sure at first what the problem was, but @hayatoito stepping through the algorithms in the Chrome bug made it more clear.
http://web-platform-tests.org/ has documentation on writing tests. It's probably worth looking at existing tests once you've got the framework running locally as the easiest way to figure out how to write them.
The other thing to do is check through the existing tests to make sure none of them test the currently-specced behavior; if they do they need to change.
Happy to help more over IRC or Hangouts or whatever once I'm in the office (~an hour or so). Although those docs should help you get started.
I opened a PR for a web-platform-tests that tests for the ShadyDOM behavior. Pending consensus in this issue :)
See w3c/webcomponents#288 (comment) by @rniwa for why we suppress in that case.
Thanks. I need some time on investigating how Blink can fire a event in such a case. I thought that is doable, but I am not 100% sure. I need to do an experiment.
I think the interoperability is the most important here. I am open to any opinions.
I have roughly implemented the behavior which I think is being proposed; when inserting a slot, and one or more slotables are assigned to the slot at this timing, we fire slotchange on the slot. As far as I experimented, Blink doesn't have any significant difficulties to implement this behavior. I can say Blink is okay to change the spec and implement it so that Blink can fire slotchange for Demo2 as well as for Demo1 in https://github.com/whatwg/dom/issues/447#issue-224427134.
If we can agree this change, I am happy to send a PR to DOM Standard, write a WPT, and update Blink's behavior. I think backward compatibility risk is very low.
WebKit's behavior is not to fire slotchange
on a newly created slot when the slot got assigned of nodes as it got inserted into the tree as I mentioned in https://github.com/w3c/webcomponents/issues/288#issuecomment-206791125. I don't know Chrome fires only in demo1 but not in demo2.
Here's my thought.
change
don't fire when the initial value is set.slotchange
eventFiring slotchange
event when slot
is inserted would make (2) harder because now we have to remember to ignore the very first slotchange
being fired. So I'm still slightly inclined towards not firing for the initial insertion.
Note: While preparing a demo for this reply, I found out that things become even more inconsistent depending on whether you create your slots in the constructor or connectedCallback
. I augmented the original table!
Slots in constructor, parser creates element | Slots in constructor, element gets upgraded | Slots in connectedCallback , parser creates element |
Slots in connectedCallback , element gets upgraded |
|
---|---|---|---|---|
Chrome | Fire | No fire | Fire | No Fire |
Safari | Fire | No fire | No Fire | No Fire |
ShadyDOM | Fire | Fire | Fire | Fire |
@hayatoito Thanks for investigating. Very glad to hear that the behavior is technically possible and feasible.
@rniwa As I said, my main goal is consistency, with a preference for changing the behavior to always firing slotchange
. Let me address these two things separately:
The behavior of slotchange
depends on:
connectedCallback
Unless you are intimately familiar with the intricacies of Custom Elements and ShadowDOM, this is going to trip up and annoy developers. When developing generic components, you might write code that works fine during development, but because users include your CE script at a different point in the page, things suddenly stop working.
A similar argument can be made for slotchange
behaving differently depending on whether you create the slot in the constructor or in connectedCallback
.
Take a look at this demo, which is a stripped down version of a tab panel component I am working on. I’m using ShadowDOM to “reshuffle” the children as a technique for graceful degradation. If JS is disabled, the tabs function as headings for the different sections. So in the markup tabs and panels are interleaved. I use ShadowDOM do group tabs and panels.
I need to be notified about new elements because I need to generate IDs and semantically link tabs and panels using aria-controls
and aria-labelledby
. Right now, I’d have to manually call into the linking code in the connectedCallback()
in addition to linking it to the slotchange
event.
I am aware that CE lifecycle callbacks are something different from ShadowDOM events, but the fact that attributeChangedCallback
does get called for initialization purposes and slotchange
does not, is confusing and somewhat unexpected. A MutationObserver would help, but ShadowDOM already has an internal MO so I think it’d be nice to be able to make use of that (apart from clearing up the aforementioned inconsistency with attributeChangeCallback
).
Those are some good points but I think we need to hear opinions of other stake holders. If they're all okay with this change, we can go ahead and make the spec change.
Definitely agree with making this change. It's much easier to create an element when the developer only has to consider one processing model for basic lifecycle.
A well written component probably have an efficient code path for when it's constructing shadow tree instead of relying on slotchange event
One could similarly argue that connectedCallback
should not run if the element is connected when the constructor runs because the constructor could just check isConnected
. As with slotchange
, this has the unfortunate effect of requiring a developer to consider multiple processing models to handle basic lifecycle. This is just fundamentally cumbersome.
Okay, given the use cases and multiple developers saying the current behavior is confusing, I'd now argue that we should make this spec change.
Sounds like noone is opposed to the change 🎉
@hayatoito Do you mind creating the PR for the spec? I’ll happily take care of the PR for the tests (w3c/web-platform-tests#5705).
Sure. Let me make a PR to DOM Standard. I'll update Blink's behavior after it gets merged.
Thank you all for making this change happen. Especially, huge kudos for @surma. :+1:
@hayatoito @surma @rniwa so should we keep suppressing for removal slots or should we just remove the concept of suppressing altogether?
I don't have any opinion about that of removing. It looks no one is interested in that.
@surma @sorvell Do you have any preference for that? If either is okay, we could remove the concept of suppressing signal altogether.
From the implementor's perspective, if we remove suppressing for slots removal, the engine will need to do an additional check (and additional potential slotchange event firing). Unless there is a good use case for slotchange at "slot removal", it might be better to keep suppressing it.
should we keep suppressing for removal slots or should we just remove the concept of suppressing altogether?
IMHO, it's better to have the same behavior in all cases: "If the nodes distributed to the slot changed, then 'slotchange' fires." I don't think the inconsistency introduced by adding the special case "However, if the nodes distributed to a slot changed because that slot was removed from a tree, then 'slotchange' does not fire." is justified by avoiding the performance cost of an extra event.
@bicknellr why not?
Okay, if someone still feels strongly we should remove suppression for slots removal I suggest they file a new issue and we can tackle that separately.
I think we should get rid of the suppression completely given that would simplify the spec & implementation. I can't think of a reason why we'd want to keep it just for the removal case.
I think I agree with @bicknellr. IINM, detaching the slot from the shadow root will remove all slotted elements (slot.assignedNodes()
returns an empty NodeList). Therefore, firing a slotchange
event seems very logical and consistent to me.
If that make suppression obsolete, I am all for removing it.
(Sorry for the late reply, I/O is happening)
@rniwa, @surma As @annevk suggested, could you file a bug separately? We might want to tackle that separately because that is not included in the original concern.
@rniwa you're not concerned with performance then? @hayatoito if everyone but us wants to remove this, we might as well keep it in this issue...
Okay. Then, let's continue the discussion. Basically, I don't have a strong opinion, however, I should be very careful for any changes which might have a performance impact and which might not have a good use case.
I think that we all are aware that getting rid of suppressing completely would simplify the spec and looks more consistent. I am aware of that too. So no need to repeat it.
My questions:
When firing such a slotevent at slot's removal, the slot's assigned nodes will be always empty. Is slotchange for that really a useful event for web developers? Does web developers have a clean up task at this timing, although that assigned nodes is always empty?
In real worlds, does removing slots (or its one of ancestors) from the tree happen frequently? If that happens frequently and there is no use case for a slotchange event at this timing, I object to this change because the performance impact can't be ignored.
When firing such a slotevent at slot's removal, the slot's assigned nodes will be always empty. Is slotchange for that really a useful event for web developers? Does web developers have a clean up task at this timing, although that assigned nodes is always empty?
I think those scenarios exist, where cleanup is desirable and/or necessary. (E.g. Setting/Unsetting aria-activedescandant
.)
In real worlds, does removing slots (or its one of ancestors) from the tree happen frequently? If that happens frequently and there is no use case for a slotchange event at this timing, I object to this change because the performance impact can't be ignored.
I have no hard numbers, but from experience I’d say that performance does not need to be a concern here. Removing slots from a Shadow Root is pretty rare – I have never seen or done it. Maybe @sorvell or @kevinpschaaf can provide additional insight.
@surma Thanks.
I think those scenarios exist, where cleanup is desirable and/or necessary. (E.g. Setting/Unsetting aria-activedescandant.)
Yeah, I can imagine. :) In addition to that, I am a little worrying about that a slotchange event for removal case would be a noise for a slotchange event listener because a slotchange listener wouldn't be interested in slotchange for "removal case" in most cases because its assigned nodes is always empty, and the slot was already disconnected from the (custom element's) shadow tree when they receive a slotchange.
It would be great if we can see a concrete example of the current usage of a slotchange event listener. Do you know that?
I guess it would be something like:
slot.addEventListener('slotchange', (e) => {
// ...
for (const slotable of slot.assignedNodes()) {
// do some task for slotable
}
}
, right? If so, I don't worry much.
I have no hard numbers, but from experience I’d say that performance does not need to be a concern here. Removing slots from a Shadow Root is pretty rare – I have never seen or done it. Maybe @sorvell or @kevinpschaaf can provide additional insight.
I think it would be difficult to have hard numbers. :) So we don't need to try to get hard numbers strictly. Rough feeling would be enough, which I had to double-check.
From the implementor's perspective, since the slot was already disconnected from the tree, I need some time to know its feasibility to support it nicely. It is different from other cases.
In any cases, I think we can land https://github.com/whatwg/dom/pull/459 separately, which shouldn't be blocked on the discussion. That is the reason I suggested to file a separate issue so that the discussion shouldn't block the https://github.com/whatwg/dom/pull/459.
Well, if we're going to change it I'd rather we'd change it all at once. Less churn for developers and less fallout. Changing how slotchange
dispatches twice in quick succession seems worse.
I got it. Thanks for the advice. Let's use this issue until the discussion settles down.
In addition to that, [...] a slotchange event for removal case would be a noise for a slotchange event listener because [...] its assigned nodes is always empty.
That’s a really good point. For example: If I wanted to use the slotchange
event to remove event listeners I added to slotted children, I wouldn’t have access to them anymore.
Maybe it’s worth thinking about exposing removedNodes
in the slotchage
event somehow. The event comes from an internal MutationObserver, so that data should be available, no? Probably a discussion for a new issue.
Regarding your code sample, @hayatoito, I think that you are right that most implementations iterate over slot.assignedNodes()
so we wouldn’t need to worry.
It would be great if we can see a concrete example of the current usage of a slotchange event listener. Do you know that?
I don‘t have any particular examples, no. Again, maybe @sorvell or @kevinpschaaf can help out?
Sorry about the late response; I think other folks here pretty much covered what I would have said though. I can only provide anecdotal evidence that the elements I've worked on don't often (ever?) rearrange their shadow tree in a way that causes their slot(s) to be removed. Most of the time, they clone and append the content of some template and that shadow tree's structure remains largely the same throughout the element's lifetime.
On a related note, after reading through the earlier comments on this thread and the related sections of the spec in a bit more detail, I learned that this event is only dispatched when the number of nodes assigned to a slot changes between zero and some non-zero amount. Was this a recent addition? I was under the impression that this event fired whenever the assigned nodes of a slot changed in any way, so I'm not sure I understand what the intended use of slotchange
is anymore. If it isn't meant to be a way to synchronously respond to all distribution changes, what is it?
I could understand if slotchange
raises the same concerns as mutation events (given that it's effectively a subset) and why that might have caused the spec change but the value seems to have been lost. If this concept is still too slow to be implemented in earnest - even with the limited scope of slotchange
- maybe it should be abandoned altogether. I'd rather have no API than a deceptively incomplete one. A custom element author could always use a MutationObserver or override appendChild
, removeChild
, insertBefore
, etc. so long as they're willing to find a reasonable workaround for children inserted during parsing or deep cloning.
No. I don't know such a recent addition. A slotchange event should be dispatched in response to all distribution changes, basically (except for some cases, like a slot removal case, which we are discussing). That should be an intended behavior. That hasn't changed, AFAIK.
For example, given:
<my-el>
<:shadow-root>
<slot></slot>
<:shadow-root>
<div id=a></div>
<div id=b></div>
</my-el>
document.querySelector('#a').remove()
triggers a slotchange on the slot, in response to a change; [a, b] -> [b].
I learned that this event is only dispatched when the number of nodes assigned to a slot changes between zero and some non-zero amount
At least in practice that is not true. Here’s a small test case.
I could understand if slotchange raises the same concerns as mutation events
Could you link me some more details of what you are referring to? I am not in the loop.
@hayatoito, thanks for pointing this out. This is my mistake, I didn't read the spec carefully enough: I looked at insert (step 6.4) and remove (step 11), which call 'signal a slot change' depending on the number of assigned nodes of a slot for changes to children of that slot, and confused these with the responses to changes to the assigned nodes of the slot. So, I ended up overlooking 'assign slotables', which is where 'signal a slot change' for changes to assigned nodes really happens and, as you mentioned, for any change to assigned nodes. Anyway, I'm now pretty convinced that the behavior of slotchange
isn't "deceptively incomplete". :)
@surma, here's the MDN page on mutation events; slotchange
reminds me of them a bit and they were deprecated a while ago because of performance problems, but I can't really speak to the history there. In a sense slotchange
acts as both DOMNodeInserted
and DOMNodeRemoved
because any insertion or removal could cause a slotchange
if the parent inserted or removed from has a shadow root with a slot. Though, my guess is that slotchange
is less expensive than these two events: mutation events had to be dispatched on any change to any node and bubble through the single tree containing all nodes in the document but slotchange
is only dispatched when the parent of the child being moved has a shadow root containing a slot and bubbling is limited only to that parent's shadow root.
Maybe it’s worth thinking about exposing removedNodes in the slotchage event somehow. The event comes from an internal MutationObserver, so that data should be available, no? Probably a discussion for a new issue.
Yeah, it is a discussion for a new issue, but I have to say Blink is difficult to support it. For a historical and the performance reason, Blink calculates slot's assigned nodes lazily. That means Blink tries to detect a slotchange is being happened at DOM mutation timing, but Blink doesn't calculate each slot's assigned nodes at this timing. The calculation of assigned nodes is delayed until some trigger would happen later, like slot.assignedNodes()
is called in an event listener.
I guess this might sound strange, but this is an important performance optimization I would like to keep, so that we don't have to remove slotchange from DOM Standard due to the performance reasons. :)
Thanks for the insight – very interesting actually. Agreed, let’s leave my remark for another time.
The bottom line is that for consistency’s sake and matching developer intuition I’d prefe we fire slotchange
on slot removal.
Okay, I roughly supported slotchange
on slot removal in Blink today. It looks doable. If I find something impossible, I'll update this issue again.
Let's wait for others' opinion for a week. If no one objects to it, let's update DOM Standard so that we fire a slotchange on slot removal too.
@hayatoito that plan sounds fine with me. Please get @domenic to review and land the change provided everyone is on board as I'll be away for four-five weeks starting in a few days.
I guess another option is to get rid of the suppression but fire slotchange
event only when the slot is inside a shadow tree. Then we wouldn't fire a slotchange
on a removed slot element since that's not in a shadow tree. That might make this event a lot easier to understand & reason.
I guess another option is to get rid of the suppression but fire slotchange event only when the slot is inside a shadow tree. Then we wouldn't fire a slotchange on a removed slot element since that's not in a shadow tree. That might make this event a lot easier to understand & reason.
That sounds reasonable to me. +1 for that.
I would like to mention that there is a minor backward incompatibility:
However, I think we can ignore this case in practical. No one is interested in this case, I guess.
If someone is worrying about this backward incompatibility, please let us know that.
One more thing:
I remember that @annevk once said something like:
Basically, the DOM shouldn't change its behavior by its root node type.
We might violate this principle, but IMO, this case could be okay. If we find that this inconsistency could be a problem in the future, we might want to relax the condition of "only if a slot is in a shadow tree". Until that, I prefer conservative approach.
It seems somewhat reasonable. I think it would be even better if slot
outside shadow trees didn't do anything else either then. No functional API whatsoever, but maybe that's more trouble than it's worth.
I see. I am okay to disable <slot>
's functional APIs completely if it is not in a shadow tree. The sounds more consistent.
As of now, the functional APIs which are visible to outside are:
<slot>
can fire a slotchange even if it is not in a shadow tree, as I explainedI think that is all. We should update DOM Standard to disable 1 and 2.
I am happy to update PR to DOM Standard if this is okay. I think this change doesn't have any practical impact on web developers.
I like this. Disabling <slot>
functionality outside of a ShadowRoot seems very reasonable to me and will simplify explaining behavior should someone run into one of the edge cases.
@hayatoito I think that change should be covered in a separate WPT for ShadowDOM. Do you want to add those?
Safari, Chrome and ShadyDOM currently exhibit different behavior when
slotchange
does and does not fire. As far as I understand the spec, it is not precisely spec’d, meaning all these current behaviors are technically spec compliant, but can be very frustrating for developers.Demo 1: Slots are created in the constructor Demo 2: Slots are creatred in
connectedCallback
Which browser fires
slotchange
under what circumstances?connectedCallback
, parser creates elementconnectedCallback
, element gets upgraded(ShadyDOM was tested using Firefox).
While my main concern is consistency across browsers, I like ShadyDOM’s behavior best in terms of DX as it is consistent with how
attributeChangeCallback
in that it fires upon initialization. This would allow developers to have their update logic in theslotchange
handler and they wouldn’t need to manually detect silently slotted elements inconnectedCallback()
./cc @hayatoito