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

Default element type(s) for <slot>? #6051

Open
LeaVerou opened this issue Oct 11, 2020 · 33 comments
Open

Default element type(s) for <slot>? #6051

LeaVerou opened this issue Oct 11, 2020 · 33 comments
Labels
addition/proposal New features or enhancements topic: shadow Relates to shadow trees (as defined in DOM)

Comments

@LeaVerou
Copy link

LeaVerou commented Oct 11, 2020

Currently, if there are more than one slot, components need to instruct authors to place the right slot="name" attributes in their light DOM elements, even when those are of very specific types.

For example, take a look at Shoelace tabs:

<sl-tab-group>
  <sl-tab slot="nav" panel="general">General</sl-tab>
  <sl-tab slot="nav" panel="custom">Custom</sl-tab>
  <sl-tab slot="nav" panel="advanced">Advanced</sl-tab>
  <sl-tab slot="nav" panel="disabled" disabled>Disabled</sl-tab>

  <sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
  <sl-tab-panel name="custom">This is the custom tab panel.</sl-tab-panel>
  <sl-tab-panel name="advanced">This is the advanced tab panel.</sl-tab-panel>
  <sl-tab-panel name="disabled">This is a disabled tab panel.</sl-tab-panel>
</sl-tab-group>

Only a <sl-tab> element will ever be in a nav slot, yet slot="nav" needs to be specified, making the markup more repetitive and error prone. Same with Elix tabs:

<elix-tabs>
  <elix-tab-button slot="tabButtons">Un</elix-tab-button>
  <div aria-label="One">Page one</div>

  <elix-tab-button slot="tabButtons">Deux</elix-tab-button>
  <div aria-label="Two">Page two</div>

  <elix-tab-button slot="tabButtons">Trois</elix-tab-button>
  <div aria-label="Three">Page three</div>
</elix-tabs>

This seems to be a rather common issue with any WC with nontrivial lightDOM contents. Theoretically the component can assign any missing slots on these elements by adding a slot attribute, but this goes against certain guidelines that instruct component authors to not add attributes on author-provided light DOM elements.

It would be good if the component author could specify a default element type for each slot, a list of types or even a CSS selector, possibly via an attribute on <slot>, so that these redundant slot attributes can be eliminated, and only used when the slot is non-obvious. Note that expanding the syntax could be done gradually: The attribute could accept an element name or comma-separated list of names at first, and potentially evolve into accepting CSS selectors if use cases arise with no backwards compat issues, since these have the same meaning as selectors too.

@domenic
Copy link
Member

domenic commented Oct 11, 2020

Note that #5483 allows the desired behavior to be accomplished in JavaScript. /cc @yuzhe-han; it would be good to get that PR finished and merged.

@domenic domenic added addition/proposal New features or enhancements topic: shadow Relates to shadow trees (as defined in DOM) labels Oct 11, 2020
@annevk
Copy link
Member

annevk commented Oct 12, 2020

For context: selectors has been considered but not done due to performance concerns. Element local name matching is feasible, but it's unclear how often it would be used in practice. If you write complicated components you probably quickly reach the limit of how useful that is. Having an API is probably a good first step here.

@LeaVerou
Copy link
Author

Thanks for the context @annevk. Note that selector matching is a spectrum. Would it help alleviate the performance concerns to only allow compound selectors? There is precedent of web platform features only allowing compound selectors for performance reasons (certain functional pseudo-classes) .

@annevk
Copy link
Member

annevk commented Oct 12, 2020

@LeaVerou ah, that might be interesting to consider.

cc @whatwg/components

@rniwa
Copy link

rniwa commented Oct 13, 2020

Thanks for the context @annevk. Note that selector matching is a spectrum. Would it help alleviate the performance concerns to only allow compound selectors? There is precedent of web platform features only allowing compound selectors for performance reasons (certain functional pseudo-classes) .

Compound selectors is exactly what v0 shadow DOM API used, and it was highly undesirable performance characteristics. As such, we wouldn't consider them.

@rniwa
Copy link

rniwa commented Oct 13, 2020

There might be, however, an argument to be made about the default slot name for a custom element like we have the default ARIA role for custom elements. If we also added the capability to just select a single element to a given slot, that would basically achieve implementing something like details and summary.

@annevk
Copy link
Member

annevk commented Oct 13, 2020

(I forgot that, but indeed: https://www.w3.org/TR/2014/WD-shadow-dom-20140617/#dfn-content-element-select.)

@LeaVerou
Copy link
Author

@rniwa That does address a majority of use cases, which are assigning a default slot to a custom element, but it does not address use cases where one wants to set a default slot for a specific type of built-in element. E.g. it does not allow implementing something like <label>. Also, as an exclusively JS-based API, it doesn't play well with SSR. Not saying it's not worthwhile (it is!), just pointing out a few limitations.

@Westbrook
Copy link

@rniwa what would be the lift of getting more up-to-date performance characteristics on this sort of approach? v0 Shadow DOM and associated learnings are 6+ years old now, and the availability of advanced CSS selectors has grown at an incredible pace in the interim. I can certainly see the perf still not being up to snuff, but I'd be interested to see if we'd at least gotten any closer in this area. Additionally, modern perf data on this would be helpful in clarifying not only this conversation, but others like w3c/webcomponents-cg#5 (comment) as well.

@rniwa
Copy link

rniwa commented Apr 9, 2021

@rniwa what would be the lift of getting more up-to-date performance characteristics on this sort of approach? v0 Shadow DOM and associated learnings are 6+ years old now, and the availability of advanced CSS selectors has grown at an incredible pace in the interim. I can certainly see the perf still not being up to snuff, but I'd be interested to see if we'd at least gotten any closer in this area. Additionally, modern perf data on this would be helpful in clarifying not only this conversation, but others like w3c/webcomponents-cg#5 (comment) as well.

I've started to think that perhaps the right approach is to decorate each custom element with a "brand", and then slot to get assignment based on that "brand".

Note the fact there are more advanced CSS selectors will only make it less likely that CSS selectors will be an acceptable way of assigning nodes to slots.

To recap, there are two primary ways using CSS selectors will pose a serious perf issue:

  • To assign a node to a slot would require matching against CSS selectors of every slot in the shadow DOM until one of them matches. To put it another way, if there are K slots in a shadow tree and there are N children of a shadow host, then the runtime of assigning nodes to slots is O(KN).
  • The order of slots would affect to which slot a given node will be assigned. Because we must assign a node to the first slot whose CSS selector matches the node, whenever a new slot is inserted, its relative order with respect to other slots must be determined, and any node assigned to slots after the newly inserted slot in the tree order (preorder DFS) may need to be re-assigned to this new slot by evaluating CSS selectors between the newly inserted slot and each slot thereafter. This, again, is O(KN). A removal of a slot would require determines whether its assigned nodes will now belong to later slots or not. With the same argument as the insertion of a slot, this too is O(KN).

With name based slot assignment, both of operations above will complete in O(1) in terms of determining whether a given node will be assigned to a given slot if there is exactly one slot of a given name (or default slot). This is critically important in order to support slotchange event as it's simply not acceptable to run O(KN) algorithm whenever a new child is inserted under a shadow host or a new slot is inserted into a shadow tree just to determine whether a slotchange event should be fired not.

@bathos
Copy link

bathos commented Apr 9, 2021

I’ve been using a pattern in most scenarios which seems aligned with what you’re describing, if I understand right — I have a helper that accepts config like [ "foo-bar", "#text" ] which would mean “text nodes and HTML foo-bar elements go here, in their original order”.

One of the reasons I do not use CSS selectors for this currently is that I consider the HTML part of “HTML foo-bar” elements important. Don’t want no eldritch void beasts entering my peaceful herd of foobs:

image

@jimmyfrasche
Copy link

An API for more complicated cases is definitely a must, but I think a lot of simple cases could be handled by a slot taking a single tag name that is only matched against top level children of the custom element. That matches how a lot of standard html elements work and seems like a good 80% solution to me. Something like <slot for="sl-tab"></slot> to use the first example in this thread.

@LeaVerou
Copy link
Author

LeaVerou commented Apr 10, 2021

@rniwa Would a (comma-separated) list of tag names work? That is compatible with selectors and can be extended in the future, would address the bulk of use cases, and seems like it could be nearly O(1) time on the number of slots and nodes if you make a map of tag names to slots that target these tag names.
I guess it also depends on how conflicts are resolved, in case multiple slots within the same shadow root target the same element type, but there are fast ways to deal with that (e.g. the first slot gets the element).

@jimmyfrasche for would not be a good attribute name for this. I'm all for re-using attribute names for similar concepts where appropriate, but the for attribute takes an id everywhere else in HTML, so defining a for attribute that takes a tag name would be very confusing for authors.

@rniwa
Copy link

rniwa commented Apr 11, 2021

@rniwa Would a (comma-separated) list of tag names work? That is compatible with selectors and can be extended in the future, would address the bulk of use cases, and seems like it could be nearly O(1) time on the number of slots and nodes if you make a map of tag names to slots that target these tag names.

Using a single element name is something I had suggested in the past but Google opposed to it / not supportive of the idea at the time. It's possible their opinions have changed since then. However, given one of the reasons @JanMiksovsky cited of liking slots better than CSS selector was that it allows subclassing to work, I'm not sure adding this specific capability makes a lot of sense especially since the idea of branding / slot kind to a custom element will work with subclassing. Note that an important use case here is to let users of a component subclass it so it's not sufficient to enumerate the list of element names in a slot.

In addition, it's not backwards compatible to add the support for CSS selector syntax after shipping element name matching mechanism using the same content attribute, and adding any kind of forward compatibility parsing support for this purpose will significant performance cost of element name matching feature.

I'm also going to reiterate Apple's WebKit team's position that simple (or other subset of) CSS selectors as a mechanism to assign nodes to a slot is not something we ever want to support. And we're saying this as the only browser engine vendor that has implemented and ships CSS JIT.

I guess it also depends on how conflicts are resolved, in case multiple slots within the same shadow root target the same element type, but there are fast ways to deal with that (e.g. the first slot gets the element).

That's what happens with named slots so we most definitely we should just pick the first one. But this still poses a perf challenge if a single slot can match against multiple element names because then we'd have to match against M element names for K slots, which will be O(KM). It's also problematic that a new element name can be added / removed from a given slot. This is akin to removing the same number of slots as there are element names specified for a given slot. It's actually worse because slots can have many to many dependencies between them. It would inevitably worsen either memory or runtime complexity or both.

@bathos
Copy link

bathos commented Apr 11, 2021

@rniwa what might the branding “keys” look like? Would it be possible for there to be keys available for native elements and for non-element node types?

I.E., it seems clear that we’d be able to express “this slot wants <some-specific-element-i-defined>” with branding, but it’s less clear to me if this solution would also permit expressing “this slot wants <li>” or “this slot wants text nodes”.

Something I’d forgotten also is that in several of these cases for me, there’s a “catch-all” slot, one which should receive everything not otherwise mapped to a specific slot, like how nameless <slot> behaves in auto assignment mode. I suspect this is a fairly common need.

(AFAICT, all of these things could theoretically be addressed within a branding API, I’m just having trouble picturing what it would look like.)

@rniwa
Copy link

rniwa commented Apr 11, 2021

@rniwa what might the branding “keys” look like? Would it be possible for there to be keys available for native elements and for non-element node types?

Maybe. In order to design what this mechanism should work, we need to enumerate a list of concrete use cases for which this mechanism will be used. We may find that in practice we'd need script-based imperative slotting anyway in which case adding this API won't be necessary. Or we might find that many use cases need an entirely different mechanism. None of that will be clear until we understand the set of use cases we're trying to address.

I.E., it seems clear that we’d be able to express “this slot wants <some-specific-element-i-defined>” with branding, but it’s less clear to me if this solution would also permit expressing “this slot wants <li>” or “this slot wants text nodes”.

What are motivating use cases for this? It's impossible to assign text nodes to any but the default slot already. What are concrete use cases in which this will come up?

Something I’d forgotten also is that in several of these cases for me, there’s a “catch-all” slot, one which should receive everything not otherwise mapped to a specific slot, like how nameless <slot> behaves in auto assignment mode. I suspect this is a fairly common need.

What are those cases? Again, it's important to explicitly enumerate concrete use cases. It's impossible to judge whatever solution / proposal we come up without having use cases to evaluate against.

@bathos
Copy link

bathos commented Apr 11, 2021

It's impossible to assign text nodes to any but the default slot already.

I wasn’t aware of this because Chrome’s current implementation does allow assigning text nodes.

What are concrete use cases in which this will come up?

For a <tree-item>, a minimal slot structure following the aria requirements ends up like:

  • arbitrary nodes (the “head”)
  • other tree-item nodes (the expandable/collapsable subtree body)

In most cases, the “head” part of the tree item would be text content, but could include other markup, including potentially interactive markup (e.g. in a file explorer, a tree item label may be editable).

Similar scenarios can arise with other “recursive” accessible widgets like menus. HTML’s native <summary> + <details> and <fieldset> + <legend> also follow this model afaict, where one kind of child node is specially placed within the shadow (or hypothetical shadow, anyway) and the rest “aren’t”. (Those cases don’t permit multiple instances of their special child, so not as good of an example.)

@rniwa
Copy link

rniwa commented Apr 11, 2021

It's impossible to assign text nodes to any but the default slot already.

I wasn’t aware of this because Chrome’s current implementation does allow assigning text nodes.

You mean with imperative API? There is no way to specify a slot name on a text node.

What are concrete use cases in which this will come up?

For a <tree-item>, a minimal slot structure following the aria requirements ends up like:

  • arbitrary nodes (the “head”)
  • other tree-item nodes (the expandable/collapsable subtree body)

In most cases, the “head” part of the tree item would be text content, but could include other markup, including potentially interactive markup (e.g. in a file explorer, a tree item label may be editable).

Sounds like either the "head" part can be inside an element of its own or a default slot can be used?

Similar scenarios can arise with other “recursive” accessible widgets like menus. HTML’s native <summary> + <details> and <fieldset> + <legend> also follow this model afaict, where one kind of child node is specially placed within the shadow (or hypothetical shadow, anyway) and the rest “aren’t”. (Those cases don’t permit multiple instances of their special child, so not as good of an example.)

What does this mean? It's unclear why existing name-based slotting doesn't work in these cases.

@bathos
Copy link

bathos commented Apr 11, 2021

You mean with imperative API?

Yes — I’m currently able to do slotA.assign(treeItemChildren); slotB.assign(everythingElse); with manual slotting in Chrome. It sounds like that will still be possible though and that I misunderstood what you meant.

Sounds like either the "head" part can be inside an element of its own or a default slot can be used?

We could. We wouldn’t because of the noise it would create for the common case. It’s almost always — but not always — a single text node.

What does this mean? It's unclear why existing name-based slotting doesn't work in these cases.

Apart from providing assurances about element-of-type slot assignees (e.g. for unguarded #private member access, assuming they don’t qualify for assignment till upgrade), name-based slotting can already address all use cases as far as I’m aware. This thread seems to concern its DX shortcomings, but DX shortcomings are subjective. I don’t think it’d be possible for me to present a use case where it isn’t possible to say “but you could have a different API that uses named slots,” right?

FWIW, we’re able to address this with fully-imperative manual assignment. It’s working well. We would switch to declared-types assignment if possible because it’d be simpler and presumably more efficient. In some cases, I suspect that switch will be possible no matter what the typed-slots API looks like. In other cases, the switch is only possible if there’s a way to have typed-slots + a default slot. If the latter isn’t in the cards, though, it’s not the end of the world, and perhaps I’m mistaken and other folks aren’t expecting to be able to do that.


Edit: I realized reviewing the last few comments that there’s probably some “drift” in here and that although I asked about cases like “this slot wants text”, I am really only concerned currently with the possibility of still having a default slot, in the same style as named-slots mode, if using typed-slots mode. When I answered about use cases, I was thinking about that, having forgotten that my question was about a more specific capability. This was probably confusing, sorry!

@rniwa
Copy link

rniwa commented Apr 11, 2021

Edit: I realized reviewing the last few comments that there’s probably some “drift” in here and that although I asked about cases like “this slot wants text”, I am really only concerned currently with the possibility of still having a default slot, in the same style as named-slots mode, if using typed-slots mode. When I answered about use cases, I was thinking about that, having forgotten that my question was about a more specific capability. This was probably confusing, sorry!

Ah, okay, yes, the default slot is still possible with "brand" based slot assignments.

@jimmyfrasche
Copy link

@rniwa I'm not really sure what brand based slot assignments entail so it's hard to evaluate. Is there something you can link me to?

@rniwa
Copy link

rniwa commented Apr 11, 2021

@rniwa I'm not really sure what brand based slot assignments entail so it's hard to evaluate. Is there something you can link me to?

I'm not proposing anything. Imagine you can specify "kind" or "brand" for a given custom element and let that inherit transparently to their subclasses. Then you assign nodes to a slot based on this "kind" or "brand". This solves two problems:

  1. You don't need to specify slot=name on each assigned element
  2. Allows subclasses of a custom element to be treated like its super class naturally

@jimmyfrasche
Copy link

I doubt I'd ever do anything with subclasses but that seems like it would solve the common case just as well as specifying the tag name for a slot and it would be transparent to the consumer so either works for me.

@bathos
Copy link

bathos commented Apr 11, 2021

Am I correct to imagine something like this would be one possible realization of such an API?:

let brand = customElements.createBrand();

customElements.define('x-y', class extends HTMLElement {
  constructor() {
    super();
    brand.register(this);
  }
});

This is not an API suggestion — I’m just looking to confirm if I understand the model being referred to as brand-based. It seems like it would need to be a WeakMap-ish thing with an imperative invocation step during construction if it is to be subclassing friendly. Is that accurate?

@rniwa
Copy link

rniwa commented Apr 11, 2021

The way I'm envisioning, it would be more like this:

customElements.define('x-y', class extends HTMLElement {
  static brand = 'my-brand';
  constructor() {
    super();
  }
});

or

customElements.define('x-y', class extends HTMLElement {
  constructor() {
    super();
    this.internals.brand = 'my-brand';
  }
});

@bathos
Copy link

bathos commented Apr 11, 2021

Ah, interesting, that is certainly very different from what I thought that term meant. This would actually not usable for us if it is a forgeable status.

@rniwa
Copy link

rniwa commented Apr 11, 2021

Ah, interesting, that is certainly very different from what I thought that term meant. This would actually not usable for us if it is a forgeable status.

Why is that? Again, it's very important for us to understand the set of use cases we're trying to address.

@bathos
Copy link

bathos commented Apr 12, 2021

@rniwa Aiming for robust and reliable internal behavior (near-)guarantees even under unusual circumstances. Standard disclaimer that I’m aware there are hard limits on how much and what kinds of soundness can be achieved in any API authored in the JS layer (this is the primary problem space I study).

Since I realize this is a very niche requirement, I don’t expect it to inform the API design decisions, at least not if doing so would have a negative impact on how the API addresses much more common needs like subclassing.

@rniwa
Copy link

rniwa commented Apr 12, 2021

I guess we can use a Symbol for this. As long as the value can't be read and it's set only, there is no way to forge it:

const secretBrand = Symbol();
customElements.define('x-y', class extends HTMLElement {
  constructor() {
    super();
    this.internals.setBrand(secretBrand);
  }
});

@rniwa
Copy link

rniwa commented Apr 12, 2021

I guess we can also use a getter/setter attribute like this.internals.brand and make its getter not return anything when it's a unique Symbol not available in the global registry.

@jimmyfrasche
Copy link

Does that mean that you need to set the brand of a slot using js too? I figured it was going to be a brand=name attr

@rniwa
Copy link

rniwa commented Apr 12, 2021

Does that mean that you need to set the brand of a slot using js too? I figured it was going to be a brand=name attr

In the case of string-based "brand" name, that should work fine.

@bathos
Copy link

bathos commented Apr 12, 2021

whoa nice — i love that, one contract transparently facilitating both the closed-over and open usage patterns

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addition/proposal New features or enhancements topic: shadow Relates to shadow trees (as defined in DOM)
Development

No branches or pull requests

7 participants