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

Shadow: Specify when slotchange fires #447

Closed
surma opened this issue Apr 26, 2017 · 64 comments
Closed

Shadow: Specify when slotchange fires #447

surma opened this issue Apr 26, 2017 · 64 comments

Comments

@surma
Copy link

surma commented Apr 26, 2017

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?

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

(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 the slotchange handler and they wouldn’t need to manually detect silently slotted elements in connectedCallback().

/cc @hayatoito

@annevk
Copy link
Member

annevk commented Apr 26, 2017

Why is it not precise?

@surma
Copy link
Author

surma commented Apr 26, 2017

@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?

@surma
Copy link
Author

surma commented Apr 26, 2017

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™️

@annevk
Copy link
Member

annevk commented Apr 26, 2017

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.

@hayatoito
Copy link
Member

hayatoito commented Apr 26, 2017

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 #229 (comment)

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.

@annevk
Copy link
Member

annevk commented Apr 26, 2017

See WICG/webcomponents#288 (comment) 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?

@surma
Copy link
Author

surma commented Apr 26, 2017

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

@annevk
Copy link
Member

annevk commented Apr 26, 2017

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.

@annevk
Copy link
Member

annevk commented Apr 26, 2017

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.

@domenic
Copy link
Member

domenic commented Apr 26, 2017

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.

@surma
Copy link
Author

surma commented Apr 26, 2017

I opened a PR for a web-platform-tests that tests for the ShadyDOM behavior. Pending consensus in this issue :)

@hayatoito
Copy link
Member

See WICG/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.

@hayatoito
Copy link
Member

hayatoito commented May 10, 2017

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 #447 (comment).

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.

@rniwa
Copy link
Collaborator

rniwa commented May 10, 2017

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 WICG/webcomponents#288 (comment). I don't know Chrome fires only in demo1 but not in demo2.

Here's my thought.

  1. Other DOM events such as change don't fire when the initial value is set.
  2. A well written component probably have an efficient code path for when it's constructing shadow tree instead of relying on slotchange event

Firing 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.

@surma
Copy link
Author

surma commented May 10, 2017

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:

Consistency

The behavior of slotchange depends on:

  • Whether you define your element before or after using the element (upgrade vs parser creation)
  • Whether you define your slots in the constructor or 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.

Always firing

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

@rniwa
Copy link
Collaborator

rniwa commented May 10, 2017

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.

@sorvell
Copy link

sorvell commented May 10, 2017

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.

@rniwa
Copy link
Collaborator

rniwa commented May 12, 2017

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.

@surma
Copy link
Author

surma commented May 13, 2017

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 (web-platform-tests/wpt#5705).

@hayatoito
Copy link
Member

hayatoito commented May 15, 2017

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. 👍

@annevk
Copy link
Member

annevk commented May 15, 2017

@hayatoito @surma @rniwa so should we keep suppressing for removal slots or should we just remove the concept of suppressing altogether?

@hayatoito
Copy link
Member

hayatoito commented May 15, 2017

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.

@hayatoito
Copy link
Member

hayatoito commented May 15, 2017

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.

@bicknellr
Copy link

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.

@annevk
Copy link
Member

annevk commented May 16, 2017

@bicknellr why not?

@annevk
Copy link
Member

annevk commented May 17, 2017

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.

@rniwa
Copy link
Collaborator

rniwa commented May 17, 2017

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.

@annevk
Copy link
Member

annevk commented May 24, 2017

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.

@hayatoito
Copy link
Member

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:

  1. <slot> can fire a slotchange even if it is not in a shadow tree, as I explained
  2. slot.getAssignedNodes({flatten: true}): This might return non-empty if the slot has a child even if the slot is not in a shadow tree.

I 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.

@surma
Copy link
Author

surma commented May 24, 2017

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?

@hayatoito
Copy link
Member

hayatoito commented May 25, 2017

@surma I will add (and/or update) WPT to cover these changes.

@rniwa
Copy link
Collaborator

rniwa commented May 25, 2017

Yeah, I really doubt that anyone is relying on slot element working when it's detached from a shadow tree.

@hayatoito
Copy link
Member

hayatoito commented Jun 2, 2017

I roughly implemented what we roughly agreed here.
From an implementation point of view, I can see that the code became much clear and simple. I think we are in a right direction.

I am now feeling that supporting <slot> in non-shadow tree is like a divided-by-zero error, which we should have excluded from the beginning. :)

Let me update PR to DOM Standard.

hayatoito added a commit to hayatoito/dom that referenced this issue Jun 2, 2017
Don't consider a slot's children as fallback contents if the slot is not in a
shadow tree.  Such a slot doesn't signal a slot change even if its children are
updated.

This patch also remove a suppress signaling flag for a slot change, and always
signal a slotchange when a slot's assigned nodes are changed.

Fixes whatwg#447.
hayatoito added a commit to hayatoito/dom that referenced this issue Jun 2, 2017
Don't consider a slot's children as fallback contents if the slot is not in a
shadow tree.  Such a slot doesn't signal a slot change even if its children are
updated.

This patch also remove a suppress signaling flag for a slot change, and always
signal a slotchange when a slot's assigned nodes are changed.

Fixes whatwg#447.
hayatoito added a commit to hayatoito/dom that referenced this issue Jun 2, 2017
A slot's children are no longer considered as the slot's fallback contents if
the slot is not in a shadow tree.  Such a slot no longer signals a slot change
even if its children are updated.

This patch also removes a concept of suppress signaling flag for a slot change
entirely. A slot change is always signaled when a slot's assigned nodes are
changed.

Fixes whatwg#447.
@rniwa
Copy link
Collaborator

rniwa commented Jun 2, 2017

Are you also updating the web platform tests?

@hayatoito
Copy link
Member

Yes, I am now working on that. Some amount of tests need to be updated.

hayatoito added a commit to hayatoito/dom that referenced this issue Jun 5, 2017
A slot's children are no longer considered as the slot's fallback contents if
the slot is not in a shadow tree.  Such a slot no longer signals a slot change
even if its children are updated.

This patch also removes a concept of suppress signaling flag for a slot change
entirely. A slot change is always signaled when a slot's assigned nodes are
changed.

Fixes whatwg#447.
@hayatoito
Copy link
Member

FYI: PR for web platform tests is web-platform-tests/wpt#5954

scheib pushed a commit to scheib/chromium that referenced this issue Jun 16, 2017
…nge concept

Rewrite the core part of the Shadow DOM v1 distribution engine so that
distribution recalculation happens *if and only if* a newly-defined concept of
a slotchange actually happens.

Benefits:

- Performance improvement
- Eliminated code complexity
- Becomes spec-compliant (as a side effect, this is one of the motivations of rewriting)

1. Performance improvement:

The result of PerformanceTests/ShadowDOM performance tests:

                v1-distribution-disconnected-and-reconnected
    Before CL:  201.4 ms
    After CL:    41.7 ms  (5x times faster)

2. Eliminated code complexity:

Though I have a plan to explain the detail about how the v1 distribution engine
works in core/dom/shadow/README.md file, let me explain the benefits of the new
design here other than the performance tests:

- Eliminated false-positive for setting a dirty flag for distribution recalculation

  Before this CL, the engine sets a dirty flag for distribution conservatively.
  As a result, a false-positive can happen, which has been difficult to avoid
  because distribution is not a local effect. We don't have much budget of time
  in DOM mutation.

  After this CL, the engine sets a dirty flag if and only if a slotchange
  actually happens. No longer needs to set a dirty flag in other places.

  Note that the engine only detects the fact of "a slotchange happens", but it doesn't
  try to know an exact distributed nodes for each slot at the timing of DOM mutation
  so that DOM mutation should not be blocked more than necessary.  Distributed
  nodes are lazily-calculated later.

- Life cycle of slot's distribution nodes became more clear

   The engine no longer clears out each slot's distributed nodes in
  shadow-including descendant subtrees when the subtree is disconnected from
  the parent tree.

   e.g. We don't need to update distribution for a custom element which has
  deeply nested other custom elements inside of it at each insertion/removal
  from a tree.

   The performance test's improvement came mostly from this result.

- Eliminated a lot of tricky code which is needed to support <slot> in non-shadow trees.

   I successfully stopped to support <slot> in non-shadow trees, and upstreamed
  the decision to WHATWG DOM Standard [1], getting agreement from other browser
  vendors [2].  I have already updated web platform tests [3] too.  These tests
  no longer fail after this CL.

   The support of <slot> in non-shadow trees has been difficult to support, and
  has been the cause of crashes.  We no longer have to fight with crashes.

3. Becomes spec-compliant (as a side effect, this is one of the motivations of rewriting)

From the Web developers' perspective, this CL shouldn't have any practical
impact, as long as a slot element is only used in shadow trees. The only
practical visible change is:

A slotchange event is always signaled as a microtask whenever a slot's
distributed nodes are changed.

For example, when a slot is inserted or removed from a tree, a slotchange event
can be fired. Before this CL, a slotchange is never fired in this case.  See
DOM Standard issue [2] for details, which is rather a feature request from web
developers.  This CL satisfies this requirement, as an intended side effect.

I am aware that it would be better to separate the engine rewriting from the user
visible changes, but it would require unnecessary efforts to keep the old behavior in the
new engine. Thus, I put all together. Supporting [2] is one of the reasons I decided to
rewrite the engine.

Note that only Shadow DOM v1 can get these benefits. Shadow DOM v0 is out of the
scope of this CL.

- [1] DOM Standard change: whatwg/dom#459
- [2] DOM Standard issue: whatwg/dom#447
- [3] Web platform tests change: web-platform-tests/wpt#5954

Links: 
Change-Id: I41f29e781185c46739377ab3939d20fa24fb69bf
Reviewed-on: https://chromium-review.googlesource.com/532734
Commit-Queue: Hayato Ito <hayato@chromium.org>
Reviewed-by: Takayoshi Kochi <kochi@chromium.org>
Reviewed-by: Kent Tamura <tkent@chromium.org>
Cr-Commit-Position: refs/heads/master@{#480013}
hayatoito added a commit to hayatoito/dom that referenced this issue Jun 29, 2017
A slot's children are no longer considered as the slot's fallback contents if
the slot is not in a shadow tree.  Such a slot no longer signals a slot change
even if its children are updated.

This patch also removes a concept of suppress signaling flag for a slot change
entirely. A slot change is always signaled when a slot's assigned nodes are
changed.

Fixes whatwg#447.
hayatoito added a commit to hayatoito/dom that referenced this issue Jul 3, 2017
A slot's children are no longer considered as the slot's fallback contents if
the slot is not in a shadow tree.  Such a slot no longer signals a slot change
even if its children are updated.

This patch also removes a concept of suppress signaling flag for a slot change
entirely. A slot change is always signaled when a slot's assigned nodes are
changed.

Fixes whatwg#447.
annevk pushed a commit that referenced this issue Jul 4, 2017
A slot's children are no longer considered as the slot's fallback contents if
the slot is not in a shadow tree. Such a slot no longer signals a slot change
even if its children are updated.

This patch also removes a concept of suppress signaling flag for a slot change
entirely. A slot change is always signaled when a slot's assigned nodes are
changed.

Fixes #447.
@trusktr
Copy link

trusktr commented Oct 10, 2017

@hayatoito

Is slotchange for that really a useful event for web developers?

It's more useful for library authors.

In real worlds, does removing slots (or its one of ancestors) from the tree happen frequently?

It's easy to imagine scenarios. Using our imagination or example, imagine a game. When the user's monster character gets to a higher skill level, it earns an extra hand. Objects like guns or tools could be placed into this new hand (i.e. slotted into this new slot). If the character is injured, maybe it's hand gets severed from battle, then the hand is removed (the slot is removed but the perhaps the gun remains in inventory).

<monster-character position="20 40 50" rotation="0 40 0">
  <weapon-rifle slot="hand-1">
  </weapon-rifle>
  <weapon-pistol slot="hand-2">
  </weapon-pistol>
  <weapon-sword slot="hand-3">
  </weapon-sword>
</monster-character>

It would be great if we can see a concrete example of the current usage of a slotchange event listener. Do you know that?

In infamous, I use slotchange to detect the composed tree. Then I can traverse the composed tree to render a WebGL scene. Traversing the light tree is (obviously) naive, and will not work when users decide to compose my custom elements using shadow dom.

A-Frame mentioned they don't intend to support Shadow DOM... But, if they did... they would need to understand the composed tree in order to render that with Three.js. slotchange will be very handy for them if they want to add this feature now or in the future.

@trusktr
Copy link

trusktr commented Oct 10, 2017

I forgot to add, slotchange events could be used, for example, to properly connect or disconnect Three.js THREE.Object3D instances from each other if those, for example, were used to render <monster-character>. Simply relying on parentNode or watching children with MutationObserver won't work, the scene won't be rendered correctly unless we consider the composed tree.

@trusktr
Copy link

trusktr commented Oct 10, 2017

At the moment, in infamous, I'm observing addition/removal of slots with MO, and using slotchange for while the slots are in DOM (my impl is here and here and some cases still need to be handled).

If slotchanged fired when slots were removed and the removed nodes were observable, I might just move to using only slotchange and no MO.

This is my perspective as someone using Web Components. If I could easily detect distributed and undistributed nodes with only a slotchange event, that might be more helpful, but I'm not completely sure yet.

It seems to me that it might be more helpful in some cases to have a distributedchildren event on shadow roots, and in the handler we could detect which slots elements went to and which slots elements were undistributed from (including being able to observe which parent a slot was removed from).

A Custom Element (a reusable component) owns its whole shadow root, so it would be okay to have shadow root events like this, because the whole logic is still encapsulated inside the Custom Element.

The event of "elements being distributed into a shadow root" seems great for more generic whole-shadow-root logic, and slotchange could still be useful for fine tuning specific logic to specific areas of the tree.

@annevk
Copy link
Member

annevk commented Oct 10, 2017

@trusktr if you want a change in behavior it would be much better to file a new issue. We're not tracking closed issues.

@trusktr
Copy link

trusktr commented Oct 10, 2017

@annevk I don't have a new issue per se, I'm just pointing out use cases that might be useful to consider (because @hayatoito had asked, but not many were provided).

After the changes in #459, how does the above table change? Is everything Fire now?

@hayatoito
Copy link
Member

@trusktr Blink has already implemented the spec change. You can confirm the new behavior in Blink. If you find a bug, I would appreciate if you can file a bug for Blink.

@dejan9393
Copy link

If anyone else is looking for a workaround for this issue on Safari, try checking for slot.assignedNodes().length in the connectedCallback

@trusktr
Copy link

trusktr commented Nov 14, 2017

I will test soon on my end when I circle back to ShadowDOM support for my lib. What should I expect from the changes? Is everything in the above table Fire, or are some of them still expected to be No Fire?

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

No branches or pull requests

9 participants