-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Comments
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. |
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). |
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. |
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
Hopefully if we can answer "yes", then this would be high-priority enough for you experts to help us write the spec? |
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. In other words, declarative APIs and imperative APIs should be mutually exclusive in one shadow tree. |
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 |
I'll post a straw-man proposal, based on my idea. |
I've posted my straw-man proposal here. I hope this can capture most use cases, with minimum changes to DOM Standard, HTML Standard, and browser's engines. |
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? |
They are never used in other shadow trees. Let me show an example.
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]); |
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? |
No. The bottom line is that assigned nodes should be the host's children. That shouldn't change. We don't relax this restriction. |
And manually-assigned-nodes too? And everyone agreed on that? Still unclear why the proposal does not state that though or why |
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. |
I've added some clarification and an example. Thanks! |
That won't quite work if we allowed 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 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 |
I'll add that we probably don't want to have a mode being set at a shadow root level. |
I think there is misunderstanding. Even if Even if the same node is added to See Example 3, where |
@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 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.: /cc @diervo |
Please explain why the answer cannot be mutation observers? They seem perfect for this case. |
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 <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 Could the proposal be extended to allow, when 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
[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 (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?) |
Thanks for the feedback. I don't have enough bandwidth to reply all today, so let me reply in the next week. @caridy [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. |
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. |
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
|
See WICG/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. |
…of its manually assigned nodes., a=testonly Automatic update from web-platform-tests Adds ability for slot to preserve order of its manually assigned nodes. Prior to this CL, the ordering of the manually assigned nodes is not preserved. Nodes were assigned in tree-order. In addition, assignment is not absolute. A slotable node can be assign to multiple slots, and where it appears is when the assigned slot first appear in a tree-order traversal, not the last slot the node is assigned to. This CL does two things. One, the order of manually assigned node is preserved. Two, assignment is absolute. The implementation uses a HeapHashMap, candidate_assigned_slot_map_, to keep track of assignment node -> slot. It uses this map during node assignment to find if another assigned slot exists. When found, the node is removed from the previously assigned slot. Preserving the ordering of manually assigned nodes is done in HTMLSlotElement::UpdateManuallyAssignedNodesOrdering(). This is called at the end of SlotAssignment::RecalcAssignment(). RecalcAssignment() walks the ShadowHost's children in tree-order, so the assigned node could be out of order from how these nodes were assigned. I thought about making a separate function for manually assigned nodes, looping though assigned nodes instead. But I decided against it at this point due to the complexity of the recalc function. For Reference: point 1 in this comment is addressed by this CL. whatwg/html#3534 (comment) Bug: 869308 Change-Id: Idc45cb593313b00f13cd5f29df8972bfe246ecce Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2103403 Reviewed-by: Mason Freed <masonfreed@chromium.org> Reviewed-by: Hayato Ito <hayato@chromium.org> Commit-Queue: Yu Han <yuzhehan@chromium.org> Cr-Commit-Position: refs/heads/master@{#757574} -- wpt-commits: 0f44bf63f0fd0b9dc883ac65297dd115f3ae9ac5 wpt-pr: 22255
Updated based on the TPAC F2F meeting in Sept, 2019. whatwg/html#3534 (comment)
I’ve been playing with this in Chrome Canary with the flag to see if it meets my needs. I think it does, so wanted to share that feedback somewhere (apologies if this isn’t the best place). I was initially worried that the microtaskiness of MutationObserver might be a problem because I have some API that exposes values which are derived from the current assignment state — and it would be weird if some sync code appends a child + checks one of these methods since it would get the stale state. However In my case, wiring this up required a lot of code! But after messing around further, I was able to abstract it away. ”Declarative imperative slotting” (...) is made possible; e.g. I suspect "I want elements of these types to be slotted in this slot" is going to be the most common usage by far, so if there is a path to make that simpler to express in the HTML API itself, it’d be awesome, but I appreciate the additional power afforded by the current API design; it’s nice to know I could make things weirder if needed :) |
@bathos, You mentioned that initially it required a lot of code to wire it up. What was the complexity and how did you simplify it? Do you have some code to share? I thought as an alternative, instead of providing a sync state check for your component, you could dispatch an async event when the component state changes. Could that made it easier? |
I can’t share the code, but I can describe what I’d ended up doing in more detail. There’s an “ElementDefinition” class in play — it’s a kinda meta builder thing, not itself a base class extending HTMLElement. I added support for a
Well, the point there was that this is not what we wanted, but yes, if we got rid of the accessors that cared about what had been slotted and made them (and everything everywhere else that depended on them in some way) into async methods instead, we would not need to use Another scenario I just recalled where the ability to ensure all assignments were “flushed” synchronously came up in (UI) event handling. In this case, the specific set of applicable keyboard and pointer events depended on whether an element was a leaf-node menuitem or a menuitem which hosted a child menu — which here meant that a tl;dr I guess is that using MutationObserver for this by default moves the act of slotting into a queued microtask, which is an awkward difference from what happens for declarative slotting, but because of |
I think that makes a lot of sense. If one uses the imperative API, they will have to write procedures manually to re-distribute nodes once a slot is added back into DOM. If there were a way for DOM to track it, then this feature would need to also be exposed for the author to be able to make decisions based on the state as well. Having something |
Right. I thought I pointed out this issue earlier somewhere but I can't find it. FWIW, this exact same issue came up during the discussion of AOM / element reflection two years ago, and we've come up with a design that works around this so that the relationship will be preserved in cases like this. See #3917
It would be good to re-use the same mechanism element reflection is using since we've just designed that thing, and it would be really regrettable to put this new API behave differently from that.
Hm... |
@rniwa I don't think everyone here is up-to-speed on the AOM discussion so if you could spell out more precisely what changes would be needed to the DOM and HTML PRs that would help a lot I think in evaluating whether that's a reasonable change to make. |
Okay, so basically the idea is that if an element A points to an element B, then that reference is conceptually permanent. However, it's visible to the script (i.e. treated as if it's been removed) if B is not a shadow-including ancestor of A (e.g. B is in an outer tree or the same tree as A). This means that even if A was temporarily removed and inserted back, it would continue to point to B as long as A is inserted into back to the same tree or another tree which can see B because A is in an inner tree. Implementation-wise, this would mean that A will have a weak pointer (e.g. weak_ptr) to B. This works because the only way B is ever accessible from A by scripts is if B is moved back into the same document and/or a script has a direct access to B, in which case B is kept alive as well. For the purpose of using this in imperative slot assignment, we probably want to add an additional requirement that the assigned relationship is only in effect if the assigned node is a child node of the shadow host of the slot. If not, then that relationship is invisible; i.e. it disappears from the DOM API like assignedNodes (e.g. when the slot is removed from the shadow tree) but re-appears if the condition becomes true again later (e.g. the slot is inserted back into the original shadow tree). |
Thanks! @domenic @yuzhe-han @mfreed7 does #3534 (comment) seem like reasonable changes to make to the imperative shadow DOM distribution API? |
So I think this does seem like reasonable behavior. In particular because it's not completely clear when slot reassignment happens, and that opaque behavior currently changes depending on where the node is located at the time. This change should make it "just work" if the assigned node is in the correct spot eventually. I agree with the comment that the layout tree, We currently issue a console warning if, at slot assignment time, the provided node isn't a child of the shadow host. That seemed helpful to developers, otherwise the provided node simply wouldn't get assigned, with no indication of why. If we change this behavior, the question arises - should we still issue this warning? Seems like yes, but thoughts appreciated. (This point obviously doesn't affect the spec.) |
ok, so we're trying to invent some 'move' semantics. I guess it makes sense here. |
Ok, I've updated the DOM PR, comments appreciated. |
Interesting. It seemed like
I did mean
... that’s how the tree of submenus was modeled. The activation behavior of menuitem changes to “open menu” by virtue of having a descendent menu slotted. Use of the same names as the aria roles may be confusing because in terms of accessible nodes, we have to end up w/ (pseudo code) |
Huh, I guess I misremembered how it worked. I guess this is another use case for WICG/webcomponents#809. |
@rniwa the solution described in #3534 (comment) for this API and for AOM will work very well for us as well! thanks for the explanation. |
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?
The text was updated successfully, but these errors were encountered: