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

Imperative shadow DOM distribution API #3534

Open
domenic opened this Issue Mar 6, 2018 · 33 comments

Comments

7 participants
@domenic
Member

domenic commented Mar 6, 2018

Writing down the stuff we talked about with @rniwa, @hayatoito, et al yesterday:

We should add a slotElement.reassign(...nodes) which reassigns the nodes.

How does this interact with the existing declarative distribution API? Maybe it just throws if the name="" attribute is specified at all; that seems nice and simple.

This API is not "perfect" because authors don't have enough hooks to call it as often as you might want. E.g. if you are trying to emulate details/summary, you will use a MutationObserver to watch for child node changes and do slotEl.reassign(theDetailsIFound). This happens at microtask timing, which is later than is done for the browser's native details/summary (people tell me that happens at style recalc timing).

But, it's pretty darn good!! I'm really excited about not putting slot="" attributes everywhere.

Any further thoughts?

@hayatoito

This comment has been minimized.

Contributor

hayatoito commented Mar 6, 2018

Thanks for filing a new issue. Yup. I think that is feasible. Let me explore API design and its semantics deeper.

I am also wondering whether there is a use case which imperative API can't address, or not. Please let us know if there is. I hope this kind of imperative API can address the rest of the world.

@annevk

This comment has been minimized.

Member

annevk commented Mar 7, 2018

Maybe it just throws if the name="" attribute is specified at all; that seems nice and simple.

That wouldn't address the elements potentially getting slotted elsewhere.

This will also require some careful study of the mutation algorithms (a fair number of which simply reset the assigned nodes).

@hayatoito

This comment has been minimized.

Contributor

hayatoito commented Mar 7, 2018

Yup, my concern is that we might have to update a lot of places in DOM Standard, depending on when we should reset imperatively specified "assigned nodes". I am sure we have to reset it somewhere.

It requires careful study.

@domenic

This comment has been minimized.

Member

domenic commented Mar 7, 2018

Thanks. I am afraid I am not enough of a shadow DOM/DOM spec expert to do that careful study. But, I will try to coordinate folks into answering the question of

I am also wondering whether there is a use case which imperative API can't address, or not. Please let us know if there is. I hope this kind of imperative API can address the rest of the world.

Hopefully if we can answer "yes", then this would be high-priority enough for you experts to help us write the spec?

@hayatoito

This comment has been minimized.

Contributor

hayatoito commented Mar 7, 2018

One thing we should decide is how declarative APIs and imperative APIs interact each other. Mixing them is troublesome and would be the cause of a confusion.

One idea to make the situation much simpler is to get "opt-in" from web developers to allow them to use imperative APIs. The scope of 'opt-in' should be a shadow tree.

e.g.
attachShadow({ mode: 'open', slotting: 'manual' /* tentative parameter name */ });

In other words, declarative APIs and imperative APIs should be mutually exclusive in one shadow tree.
We don't mix them in the same shadow tree. That would make the situation much simpler, I think.

@annevk

This comment has been minimized.

Member

annevk commented Mar 7, 2018

I don't think that necessarily helps, since if you have another shadow root that's not manual you'll still run into many of the same questions. Either way we'll have to deal with what happens when a node is already assigned.

cc @whatwg/components

@hayatoito

This comment has been minimized.

Contributor

hayatoito commented Mar 7, 2018

I'll post a straw-man proposal, based on my idea.

hayatoito added a commit to w3c/webcomponents that referenced this issue Mar 8, 2018

@hayatoito

This comment has been minimized.

Contributor

hayatoito commented Mar 8, 2018

I've posted my straw-man proposal here.

https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Imperative-Shadow-DOM-Distribution-API.md

I hope this can capture most use cases, with minimum changes to DOM Standard, HTML Standard, and browser's engines.

@annevk

This comment has been minimized.

Member

annevk commented Mar 8, 2018

So what happens if you have two (parallel) host elements and I manually assign children (its slotables) from the first host element to the shadow tree slots of the second? How does that not run into the issues I alluded to earlier?

@hayatoito

This comment has been minimized.

Contributor

hayatoito commented Mar 8, 2018

They are never used in other shadow trees. Let me show an example.

host1
├──/shadowroot1 (slotting=manual)
│   └── slot1
└── A

host2
├──/shadowroot2 (slotting=manual)
│   └── slot2
└── B
slot2.assign([A]);
assert(slot2.assignedNodes() == []);

slot1.assign([A]);
assert(slot1.assignedNodes() == [A]);

shadowroot2.append(slot1);
assert(slot1.assignedNodes() == []);

shadowroot1.append(slot1);
assert(slot1.assignedNodes() == [A]);
@annevk

This comment has been minimized.

Member

annevk commented Mar 8, 2018

But wasn't the point of the imperative API that you were not restricted on where the elements came from? At least, it sounds like you want them to be restricted to children of the host element? How does this follow from your document?

@hayatoito

This comment has been minimized.

Contributor

hayatoito commented Mar 8, 2018

No. The bottom line is that assigned nodes should be the host's children. That shouldn't change. We don't relax this restriction.

@annevk

This comment has been minimized.

Member

annevk commented Mar 8, 2018

And manually-assigned-nodes too? And everyone agreed on that? Still unclear why the proposal does not state that though or why assign() succeeds despite failing.

@hayatoito

This comment has been minimized.

Contributor

hayatoito commented Mar 8, 2018

I didn't introduce any restriction to manually-assigned-nodes. Any programmer's mistake can be okay there by design. Invalid nodes in manually-assigned-nodes are never selected as assigned nodes. assigned nodes are only observable.

I think this design choice would make the standard and the implementation much simpler.

Anyway, let me state that clearly.

Any alternative ideas are welcome, of course. I would like to hear feedback.

@hayatoito

This comment has been minimized.

Contributor

hayatoito commented Mar 8, 2018

I've added some clarification and an example. Thanks!

@rniwa

This comment has been minimized.

Collaborator

rniwa commented Mar 8, 2018

That won't quite work if we allowed slotElement.reassign to select a non-direct-child descendent of a host element in nested shadow trees, or if we allowed some arbitrary node elsewhere in the trees and had two shadow trees.

Consider when the case when (x) is manually assigned of (d). In that case, (d) would belong to both (x) and (y), which shouldn't be allowed.

a (outer host) ---- SR (manual)
  + b                  + slot (x)
  + c (inner host) --- SR (auto)
      + d                 + default slot (y)
      + e 

I think a lot simpler model is to keep track of the slot to which a given node is manually assigned, and forbid declarative slotting from picking that node.

Namely, each node will have an internal slot manually slotted flag, which is initially set to false. Each time slotElement.reassign is invoked (and when the slot is removed from a shadow tree, etc...), we update this flag's value. The existing slotting algorithm would simply ignore any node with this flag set to true.

@rniwa

This comment has been minimized.

Collaborator

rniwa commented Mar 8, 2018

I'll add that we probably don't want to have a mode being set at a shadow root level. details element, for example, wants to use the default slot to get everything but the first summary.

@hayatoito

This comment has been minimized.

Contributor

hayatoito commented Mar 8, 2018

Consider when the case when (x) is manually assigned of (d). In that case, (d) would belong to both (x) and (y), which shouldn't be allowed.

I think there is misunderstanding. Even if slotX.assign(d) is called, slotX.assignedNodes() doesn't contain d, according to my proposal. d is never used as assigned nodes of slotX because d is not the host a's child.

Even if the same node is added to manually-assigned-node in more than one different slots, the node should appear at most one slot's assigned nodes in my proposal.

See Example 3, where A is manually assigned to slot1 and slot2, however, A does not show in slot2's assigned nodes.

@caridy

This comment has been minimized.

caridy commented Mar 9, 2018

@hayatoito, the proposal is looking good from the perspective of doing the manual allocation, but it is not providing enough information, or end-to-end examples.

My main concern is that with manual, no auto allocation/slotting will be done, which probably mean that you cannot use slotchange event to observe changes in your slots. The question is, how will the developer know when to do the manual allocation? And the answer cannot be: "just use mutation observer".

I think from our side, we will like to have a reliable signal for changes on the light tree so the allocation can happen accordingly. The most dummy example could be:

<fancy-menu>
     <menu-item>one</menu-item>
     <menu-item>two</menu-item>
</fancy-menu>

When adding a new menu item (e.g.: myFancyMenuElement.appendChild(menuItemThreeElement)), we should be able to detect that operation, and take care of the manual slotting on the spot.

/cc @diervo

@domenic

This comment has been minimized.

Member

domenic commented Mar 9, 2018

Please explain why the answer cannot be mutation observers? They seem perfect for this case.

@JanMiksovsky

This comment has been minimized.

JanMiksovsky commented Mar 9, 2018

I can envision scenarios in which a developer would like to mix slotting by name and/or default slotting with manual slotting.

Suppose an app has a menu that shows menu items, some of which are conditionally available. Perhaps it supports markup like:

<menu-element>
  <div slot="caption">Menu</div>
  <menu-item show-when="signedout">Create account</menu-item>
  <menu-item show-when="signedin">Account settings</menu-item>
  <menu-item>Help</menu-item>
</menu-element>

And suppose menu-element has a template like:

<template>
  <slot name="caption"></slot>
  <slot name="availableItems"></slot>
</template>

This menu element wants to leverage preexisting support for slotting by name — in this situation, slotting a caption into the caption slot is easily managed by adding name="caption". At the same time, the menu wants to manually assign the appropriate menu items to the availableItems slot based on a condition evaluated at runtime.

Could the proposal be extended to allow, when slotting is manual, nodes to be assigned to named slots as usual? The idea is that named slotting happens first, then the dev can do what they want.

Along the same lines, I could imagine wanting to have a default slot still serve as the default destination even when manual slotting is being used for specific slots. It would be useful for a component to manually pluck the nodes it wants to assign to specific slots, then let everything else fall into the default (unnamed) slot.

In short, rather than having manual mode entirely disable existing behavior, the new imperative API could extend it. As I read the proposal, the new proposed "Find a slot" step could drop the "If shadow's slotting is manual" condition:

  1. [New Step] Return the first slot in shadow’s tree whose manually-assigned-nodes includes slotable, if any, and null otherwise.
  2. Otherwise, return the first slot in shadow’s tree whose name is slotable’s name, if any, and null otherwise. (<= No change)

[Note: the existing text of step 6 above fails to mention the default slot, but probably should.]

If such an accommodation could be found, it might allow the introduction of an imperative slotting API without needing to introduce a slotting mode at all.

(Side note: If it'd be helpful to fork this topic into a separate issue, I can do that. Perhaps someone who can create labels on this repo could set up a label for imperative distribution?)

@hayatoito

This comment has been minimized.

Contributor

hayatoito commented Mar 9, 2018

Thanks for the feedback. I don't have enough bandwidth to reply all today, so let me reply in the next week.
I have a plan to add "Alternatives Considered" to the proposal.

@caridy
Our assumption is that users can use MutationObservers.

[Update: I understood that the following explanation is not directly related to @caridy's concern, but let me keep the following explanation, as a side note, because the concept of manually-assigned-nodes is likely to cause a confusion to end-users.]

We still auto-calculate assigned nodes for each slot in a shadow tree, using the information of manually-assigned-nodes users gave for each slot. manually-assigned-nodes is just a hint for an engine. The engine can't trust manually-assigned-nodes as is because manually-assigned-nodes may include an invalid node which can't be used as a member of assigned nodes.
If assigned nodes are changed as a result in some slots, slotchange is fired at the end of microtask timing for the slots even when they are in manual.

@rniwa

This comment has been minimized.

Collaborator

rniwa commented Mar 9, 2018

I think there is misunderstanding. Even if slotX.assign(d) is called, slotX.assignedNodes() doesn't contain d, according to my proposal. d is never used as assigned nodes of slotX because d is not the host a's child.

Okay, then your proposal doesn't satisfy a major use case of the imperative API to select a non-child node of a shadow host. I don't think that's okay. An imperative API should allow slotting of an arbitrarily deep descendent node.

@domenic

This comment has been minimized.

Member

domenic commented Mar 9, 2018

It would help to have some concrete examples of elements that want to slot a deep descendant. I'm unsure if select/optgroup/option would count here (i.e., would select slot both options and optgroups, or would it slot its children, and then optgroup slots its children).

Nevertheless, I agree it seems like something we should be aiming for, especially if we want to fulfill

I hope this kind of imperative API can address the rest of the world.

@rniwa

This comment has been minimized.

Collaborator

rniwa commented Mar 9, 2018

See w3c/webcomponents#574 for a concrete example.

Just to be clear, we'd be opposed to any imperative API proposal which doesn't support this use case since we see this as one of the primary motivations for having imperative API at all. In fact, the only reason we receded our proposal to support this in declarative syntax was one of Google representatives made an argument that we can support this in imperative API.

@hayatoito

This comment has been minimized.

Contributor

hayatoito commented Mar 12, 2018

Regarding w3c/webcomponents#574 (slotting indirect children),

I remember w3c/webcomponents#574, where I pointed out why "slotting indirect children" is a non-starter from the theoretical and practical perspective, however, I didn't get any response on that. It looks that only I have explored this problem space so far.

To avoid answering the same question repeatedly, it would be better to share my insights clearly here. Please read these and think carefully by yourself before filing a request for "slotting indirect children".

First of all, supporting "slotting indirect children" is NOT a nice-to-have feature. It is a sort of an attempt to introduce "division-by-zero" to the beauty of the Shadow DOM world. That is infeasible.

In other words, "Only host's children can be distributed" is MANDATORY. That has been the fundamental requirement to make Shadow DOM work, from the very early days of Shadow DOM.

For those who will explore this problem from now, let me share several things which you will encounter, hoping this would be helpful to understand what problems you are trying to solve:

1. Think your proposal's impact on the event path.

Think the following simple case.

host1
├──/shadow-root
│   ├── E
│   │   └── slot1
│   └── F
│       └── slot2
└── A
    └── B
        ├── C (slot=slot1)
        └── E (slot=slot1)

And think how you can update get the parent algorithm so that weird things shouldn't happen, such as:

  • child, C, receives an event, however, its ancestor nodes, A or B, doesn't receive the event
  • If you resolve this issue naively, A would have more than one parent nodes, regarding an event path. A's parents would be slot 1 and slot2. That would depend on the context; where an event happens. "Tree" behind the event path is no longer "Tree".

2. Think how your proposal work with: Two nodes are assigned to the same slot (or different slots) in the same shadow tree, however, one node is an ancestor of the other.

3. Think your proposal's impact on a nested web components case

host1
├──/shadow-root
│   └── slot1
└── A
    └── B
        └── C
            └── C
                └── D
                    └── E
                        └── F (-> slot1)

The flat tree would be:

host1
└── slot1
    └── F

Think what happens: attach shadow to B (and append slot2 to B' shadow root)

host1
├──/shadow-root
│   └── slot1
└── A
    └── B
        ├──/shadow-root
        │   └── slot2
        └── C
            └── C
                └── D
                    └── E
                        └── F (-> slot1)

  • Think about the relationship between slot1, slot2 and F.
  • In addition to that, think what happens if D is assigned to slot2 later
host1
├──/shadow-root
│   └── slot1
└── A
    └── B
        ├──/shadow-root
        │   └── slot2
        └── C
            └── C
                └── D  (-> slot2)
                    └── E
                        └── F (-> slot1)

Think about more complex scenerio and how your concrete proposal can have a reasonable answer for that, from the performance's perspective. e.g.. to avoid O(n) traversing.

4.Think the use case

The use case in w3c/webcomponents#574 didn't make sense to me. I pointed out, "Remove unnecessary <my-tab> from the markup" there. <my-tab>'s role there is just a comment node in the markup, effectively.

Only remaining appealing point for that seems to me:

For example, if you want to remove, move or add a tab, then you could do this with one command.

However, in general, DOM doesn't allow inserting such a "no-op" comment container node in the markup.
If you need such a "no-op" comment container node in your markup, you should file an issue for DOM, instead of here. It is unclear to me why only Shadow DOM should support such a weird requirement.

And, I've never heard such a weird requirement from actual users of Web Components, such as Polymer team.


I have showed a couple of examples here, however, I am pretty sure these are not only things which would be broken. So please try to have a concrete proposal, instead of just replying to each example. Please don't let me guess how your proposal would be. That would be unproductive for us.

I am happy to review a proposal if someone has explored this problem space deeply and still can have a concrete proposal which can support "slotting indirect children".

@rniwa

This comment has been minimized.

Collaborator

rniwa commented Mar 12, 2018

The assertion that not being able to assign a non-direct child to a slot is a requirement for shadow DOM is utterly false.

In fact, I've explored this problem space and prototyped such a model. I can post a detailed proposal later (not possible in the next few weeks or months due to other commiments) but I figured you can sort it out yourself; I gusss not.

My earlier comment withstands. Any proposal for an imperative slotting API that doesn't support assigning a non-direct child is a show stopper for Apple. It was a show stopper four years ago, and it is a show stopper today.

I'm quite surprised that we're having this conversation again because I felt like we made our position very clear then. It's one issue we can't compromise.

@domenic

This comment has been minimized.

Member

domenic commented Apr 2, 2018

Since @rniwa is not able to help us for the next few months, I am wondering if it's worth moving forward with a child-only version, that can then in the future be extended to support more descendants when @rniwa has time to help us figure out the model?

I understand it's a must for @rniwa to solve arbitrary descendants. But children are a subset of descendants, so if we can come up with a subset proposal that can be extended in the future, we may be able to make progress instead of stalling for months.

@annevk

This comment has been minimized.

Member

annevk commented Apr 3, 2018

That seems somewhat unrealistic given that @hayatoito is unclear on how a descendants-solution would work.

@domenic

This comment has been minimized.

Member

domenic commented Apr 3, 2018

Right, but apparently @rniwa is clear apparently, so once he is able to find the time, he can extend the child-only solution to work with descendants.

@annevk

This comment has been minimized.

Member

annevk commented Apr 3, 2018

What makes you think they're compatible though?

@domenic

This comment has been minimized.

Member

domenic commented Apr 3, 2018

Well, a child is a special case of a descendant.

@trusktr

This comment has been minimized.

trusktr commented Sep 15, 2018

Could a concept more similar to "portals" (which various frameworks today have, f.e. React portals and Vue portals) possibly be better, as alternative to the proposed deep slotting?

It would be great for whatever solution to be super obvious (explicit) about when your element is rendered to another place besides the immediate parent, and not unexpected.

For this to be true, it may be the mechanism has to be only imperative and reference based. It would be great if only a Custom Element could explicitly do teleporting to some portal by reference, perhaps using a private API (see w3c/webcomponents#758 about giving APIs to CE authors).

Basic idea:

<body>
 <div>
  <portal></portal>
 </div>
 <div>
  <my-el></my-el>
 </div>
</body>
// Inside MyEl class
const portal = document.querySelector('portal')

private(this).teleport(portal) // my-el renders relative to where portal is located

(Private class fields are coming soon.)

This idea is not tied to ShadowDOM. But for it to work in ShadowDOM, a component author would place it in a root then expose the portal reference to the outside.

A (grand)child element of the component could traverse up the tree to find the component from which to get the reference from (f.e. in connectedCallback).

Outside code cannot get a reference to the my-el and make it teleport, unless the element makes the teleport mechanism public. This allows for creating the guarantee that the element will explicitly know where it teleports to without surprises. However the CE author can decide to forgo this guarantee by exposing the teleport mechanism to the outside.

The connection is made purely imperatively (the .teleport feature), no declarative option.

@joelrich This would work for your case in w3c/webcomponents#574 I think.

(I know this is not a proposal, I'm just throwing in an idea to get cogs turning)

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