-
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
Revamped Scoped Custom Element Registries #10854
Comments
So to this.shadowRoot.customElements.createElement('my-el'); Is that correct? |
Will the global function cloneInScope(src: DocumentFragment, scope: Document | ShadowRoot | Element = document) {
const registry = scope.customElements ?? globalThis.customElements;
return registry.cloneSubtree(src);
} Assuming the above, then at first glance, this proposal looks like it will enable all my scenarios. |
@EisenbergEffect : Yes, |
Yes although a more convenient way is to use whatever node you already have in the shadow tree and do: node.customElements.createElement('my-el'); |
Right so from “inside” a custom element you could either do Additionally, I think the addition of allowing a registry for a node is a good addition 👍 |
Why happens in this scenario? <outer-element>
<template shadowrootmode="open">
<inner-element></inner-element>
</template>
<outer-element>
<inner-element></inner-element> Assuming that there are different definitions for |
@matthewp : in that scenario, all the elements will use the global registry since there is nothing on |
@rniwa Thanks, that was my suspicion. That seems like a show-stopper to me. Can we add something to |
In the proposal @annevk made above, there is |
Thanks very much for working on this @rniwa and @annevk! I think this proposal is an improvement over the original. I have a few questions and refinement suggestions. initializing a registryIt seems problematic to expose this only via <div id="host">
<template shadowrootmode="open" shadowrootcustomelements="">
<x-foo></x-foo>
</template>
</div> Putting an API to initialize a registry on I also think it would be great to be able to create an imperative shadowRoot with a blank Might this be workable? const shadowRoot = element.attachShadow({mode: 'closed', customElements: null});
shadowRoot.innerHTML = `
<x-foo> <x-bar></x-bar>...</x-foo>
<x-foo> <x-bar></x-bar>... </x-foo>
`;
const xFoo1 = shadowRoot.firstElementChild;
const xFoo2 = shadowRoot.lastElementChild;
xFoo1.customElements = registryA; // upgrades XFoo1 and its subtree in registryA?
shadowRoot.customElements = registryB; // upgrades XFoo2 in registryB?
//
xFoo1.customElements = registryB // throws. cloning
console.assert(element.customElements == registryA); // ok
const clone = element.cloneNode(true);
console.assert(clone.constructor == registryA.get(clone.localName)) // ok?
|
Thanks for making this revision @annevk and @rniwa. I'm very glad that it seems like we can just have elements remember their registry and not have to always defer to shadow roots! A few questions / concerns: Element creationI think that in order to get frameworks and rendering libraries to support for scoped registries we have to make it extremely easy and performance-neutral for them to add. The way I had proposed this was to add Because ShadowRoot's optionally had an associated CustomElementsRegistry and fell back to global creation when they didn't have one, a library could always use the shadow root as the creation object, and fall back to the document when not rendering into a shadow root. This simplifies element creation a lot - there's little code or perf overhead to supporting scoped registries. For example, In lit-html, we pass either // Use the global scope always. (document is also the default)
render(html`<x-foo></x-foo>`, {creationScope: document});
// In a web component, use registry of the shadow root, which may or may not have a scoped registry
render(html`<x-foo></x-foo>`, {creationScope: this.shadowRoot}); On the library side, support for scopes is as simple as: const fragment = (options?.creationScope ?? document).importNode(template.content, true); I worry that an API like I think it'd be an easier lift if instead we made it possible to use a Document or ShadowRoot in more cases as a scope object. They have other useful common APIs like This wouldn't preclude element creation APIs from also existing on CustomElementsRegistry. Non-shadow DOM usage and SSRI think that like <body>
<x-feature-1>
<x-foo></x-foo>
</x-feature-1>
<x-feature-2>
<x-foo></x-foo>
</x-feature-2>
</body> Where |
The way we are envisioning this API will be used is that we'd use CustomElementRegistry as scoping object instead of ShadowRoot. It's more natural that way since you'd often construct a tree without necessarily having access to the future root node. You can easily fallback to document like this: It's possible to add convenience functions on ShadowRoot as well but it did seem like something we can wait for the community feedback. |
It's possible to extend this API to support an element with null registry in a document tree as a thing by introducing a new content attribute for the parser to consume but I don't think we should include that in the initial version unless we can find very important use cases that require that. |
Is that a common scenario? I can't think of a situation in which author uses a declarative DOM in conjunction with a scoped custom element without using a custom element on the shadow host. What are concrete use cases? |
We had considered that option but concluded that a setter which allows setting once then starts throwing is an exotic behavior we want to avoid. We also had hopes to make it so that elements are never exposed to scripts until its registry is initialized. However, now we realize this is not possible since end user could interact with such an element and trigger a composed event before scripts had a chance to define its registry (or else it sort of defeats the whole point of SSR). So given that, we can revisit this alternative design. |
The primary way cloneSubtree differs from importNode is that it does deep cloning by default as Mozilla had advocated in the past (since that's what you want in most cases anyway) when we were standardizing cloneNode's default argument to be optional. We thought using the same method name would be confusing given that distinction. |
We've seen a lot of interest in using scoped registries for micro-frontends (MFE) where a subtree might be managed in a framework that either doesn't use custom elements and/or doesn't want Shadow DOM. The general problem with these MFE use cases is that tend to be very over-constrained so the platform must be expressive and flexible to handle them. We can definitely try to get more feedback on these issues. Consider this scenario... <div id="svelte-app">
<template shadowrootmode="closed" shadowrootcustomelements>
My svelte MFE
<design-system-button>version 1.2.3 so must be that registry</design-system-buttton>
</template>
</div>
<div id="vue-app">
<template shadowrootmode="closed" shadowrootcustomelements>
My vue MFE
<design-system-button>version 1.1.7 so must be that registry</design-system-buttton>
</template>
</div>
<div id="react-app" customelements="">
My react MFE (needs global styling!)
<design-system-button>version 1.3.8 so must be that registry</design-system-buttton>
</div> |
One reason I (mildy) prefer at least the option of using a ShadowRoot as the scoping object is that it has other scope-related APIs, like Of course, as you point out, both Document and ShadowRoot would have I know similar arguments were made about the Another issue for me is (root.customElements?.cloneSubtree?.(template.content) ?? document.importNode(template.content, true) vs (root.importNode ?? document.importNode)(template.content, true) It might not seem like much, but we've seen pushback over similar things. I also have a question about the value
One nice thing about |
I can see MFE may not want to use shadow DOM. But the combination of waiting to use shadow DOM and scoped custom element registry but not custom elements for the host seems like odd combination to me. What are examples of frameworks / libraries / websites that do this? |
To us, it seemed weird that
That might be an argument for making
Over time (with any polyfill), the former will simplify to just
It points to the global registry.
That is tautologically true of |
It seems really odd to me to have |
here’s the MFE use case that is of interest to me. as usual it’s a design system use case. let’s say i have a design system written in web components. so all my buttons, modals, tooltips, etc are custom elements but the app i’m writing is a react app that consumes those elements. and my app is an MFE remote app that gets loaded async on the same page as another MFE remote app also in react AND the “host” app which is the parent of all the MFE remotes. my app is not the whole page shown to users, but just a part of it. and my app is rendered by react (a shared dependency from my app contains v1.0.0 of the design system button, x-button. and the host app has v0.0.2 of x-button and another MFE remote app has v2.0.0 of x-button all at the same time. scoping is needed so we go to set it up so that my MFE app version of the design system components can’t conflict with the host app or other MFEs remote apps that might be rendered into the same page as my MFE remote. under the existing proposal, i’d have to:
it would be easier if registries and shadow roots were disconnected because i wouldn’t have render my MFE app in shadow root. if there was a way to programmatically just “apply a registry to some div perpetually” then an MFE setup could just create the registry and the react render root separately with js, then link them together without having to involve react internals at all. if i could do something like: const registry = new Registry();
//add els to registry
registry.define(‘x-button’);
const root = React.createRoot(‘div’);
// tell the root that all WCs in it should use the registry first, global as a fallback
root.attachRegistry(registry);
root.render(<MyApp/>); and not involve react at all that would be amazing for MFEs |
I agree with @michaelwarren1106 like I tried to make clear in discord I would love to have a similar way to how forms work currently if you wrap a form around input elements they register to that unless you set the form attribute on the input to something else. You can also apply this attribute form I think to an input outside the form tag. <!-- this registereds all to the form / could this method also work for custom elements? -->
<form id="myForm" action="/submit" method="post">
<button type="submit">Submit</button>
<input type="password" name="password" placeholder="password">
</form>
<input type="text" name="username" form="myForm" placeholder="Username">
<input type="text" name="email" form="myForm" placeholder="Email"> |
From the Salesforce perspective, my initial feedback is that this new proposal looks fine. In our use case, we tightly control which scripts have access to which runInScope(registry, () => {
// code that may be creating elements with `document`, `innerHTML`, etc.
}) So either way, we're going to have to hook up all the wiring so that element creation goes to the "right" registry. Whether it's attached to the element or the shadow root is kind of irrelevant to us. The question on our side is whether we'd even allow scripts to actually use these new APIs, or if we'd just block them entirely. Probably for a v1 we'd do the latter, especially since that's what would work for 100% of existing code anyway. (Note we are not just working with first-party code: we build a platform for second- and third-party code.) Removing my Salesforce hat for a moment, I am still kind of concerned (as I was with the previous API) that, to build an effective MFE on top of this, you'd need to get every framework / script / library on board. And if everyone isn't playing ball, then you can easily escape the MFE boundary and cause mayhem. It's a very tough coordination problem that people are largely solving with That said, if |
@rniwa do you think WebKit might be interested in putting this proposal behind a flag in Tech Preview so that WCCG members might experiment with this before the Web Components Face-to-Face in Jan/Feb to empower this conversation? |
Maybe! That would be great indeed. |
partial interface Element {
readonly attribute CustomElementRegistry? customElements;
}; Why |
I think it would return |
It likely will return null in all cases where there's no browsing context and it's not been inserted into a tree with a non-null registry parent. Working out the DOM and HTML PRs now for review that will define all of this in full detail. |
Do not comment directly on this PR while it is in draft state. Use #10854 instead. DOM PR: whatwg/dom#1341. Tests: ...
I've put up draft PRs. Please do not comment directly on them. That makes them far too unwieldy. They both identify where you can leave comments. New issues are also fine. DOM side: whatwg/dom#1341 |
@annevk Is the purpose of Or otherwise - should insertion of a shadowroot into a scoped-registry-owning shadowroot run upgrade? |
I'm not sure this could ever happen since elements are created always with a known registry. The exception is a shadowRoot with a null registry, but this can only be set via |
@keithamus it's set when a declarative shadow root has its @rniwa and I also discussed some of the feedback on |
This comment was marked as duplicate.
This comment was marked as duplicate.
@annevk To me one of the key improvements of this spec is that the need to "to look up a registry on insertion" could completely go away. The only case that a registry needs to be set should(?) be:
This (3) case is tricky. For web compat. (I think), this must default to the global registry. I would proposal that if you do EDIT: If the above rules make sense and we do endeavor to remove "look up registry on insertion," we can open a separate issue for how x-document should work |
The current proposal is that for a connected insert we'll set each inclusive descendant's registry (if it's null) to that of its parent. And if it's a ShadowRoot (whose keep custom element registry null is false) to that of its host. In certain cases this might mean it remains null, but that's okay. And generally this preserves compatibility with today's behavior. You can use |
What are the differences between |
I think we need these semantic distinctions. I'm not entirely happy with the naming though and I suspect we want to make the above tweak to |
DOM PR: whatwg/dom#1341. Tests: ... Closes #10854.
Hello. I recently started using scoped registries. First, here's a complete example to consider: <my-parent>
<shadow-root>
<my-input></my-input>
<my-child>
<shadow-root>
<my-input></my-input>
<sl-button></sl-button>
</shadow-root>
<my-child>
</shadow-root>
</my-parent> Currently, I found two limitations.
I think both scenarios are related but also pulling in opposing directions. IMO, at any given level in DOM tree, if an element is not defined, it should be possible to have a scoped registry to up the tree and either find the definition in a parent node's scoped registry, or fall back to the global registry. |
Shoelace has moved to a pattern where elements are not registered by default and you either register then in a scope yourself, or import a different module that registers them globally. With this setup, my-child should import and register sl-button in it's own scope and not use the global registration. I expect most component libraries to move to this pattern. |
https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Scoped-Custom-Element-Registries.md is a good proposal, but it ties the functionality too much to shadow roots. This is Ryosuke and I's proposed improvement attempting to account for feedback given in various Web Components issues on this topic: https://github.com/WICG/webcomponents/issues?q=is%3Aissue+label%3A%22scoped+custom+element+registry%22.
First, the IDL, illustrating the new members:
Here’s a summary of how the proposal evolved:
CustomElementRegistry
still gains a constructor.ShadowRoot
still supports aCustomElementRegistry
, exposed through acustomElements
getter.shadowrootcustomelements
attribute, which is reflected as a string for forward compatibility.ElementInternals
gainsinitializeShadowRoot()
CustomElementRegistry
gainsinitializeSubtree()
so a declarative shadow root (or any element) can have itsCustomElementRegistry
set (when it’s null).attachShadow()
member is now calledcustomElements
for consistency.Element
should support an associatedCustomElementRegistry
, exposed through acustomElements
getter. This impacts elements created throughinnerHTML
and future such methods, such assetHTMLUnsafe()
. This will allow using non-globalCustomElementRegistry
outside of shadow roots.setHTMLUnsafe()
in the future could maybe also set its ownCustomElementRegistry
. Given the ergonomics of that it makes sense to expose it directly onElement
as well.CustomElementRegistry
should gain acreateElement()
. It should fallback to creating built-in elements. Any element created this way will have theCustomElementRegistry
associated with it.document.createElement()
:CustomElementRegistry
should gain acloneSubtree()
method that clones a node and upgrades it and its children using the registry.I’ll create specification PRs as well to allow for review of the processing model changes. We believe this resolves the remaining issues with the latest iteration of the initial proposal.
I'd like to briefly go over this in the December 19 WHATNOT meeting and will also be available then to answer any questions. Marking agenda+ therefore.
cc @rniwa @justinfagnani @whatwg/components
Minor issue tracker:
createElement()
tocreate()
ascustomElements.createElement()
reads rather redundant.initializeSubtree()
perform upgrades in the connected case? (If yes, it should probably also update the scoped document set.)initializeSubtree()
? (Also consider the names already exposed on this object.)The text was updated successfully, but these errors were encountered: