-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Revamped Scoped Custom Element Registries #10854
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
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"> |
DOM PR: whatwg/dom#1341. Tests: ... Closes #10854.
DOM PR: whatwg/dom#1341. Tests: ... Closes #10854.
Not a big issue (more like a detail), but it looks like with the proposal in the current form, it's not possible to have an associated IMO, it should either inherit the custom element registry of the |
You can use |
Oh, thanks, overlooked that! |
@tbroyer good point. web-platform-tests/wpt#51976 |
This implements the IDL definitions per the spec issue[1], however all methods are basic stubs and do not execute any logic - the logic will come with later patches. I2P: https://groups.google.com/a/chromium.org/g/blink-dev/c/um-9YjJWyEQ/m/MhKN0L7FAgAJ Spec PRs: HTML: whatwg/html#10869 DOM: whatwg/dom#1341 [1]: whatwg/html#10854 Bug: 409577162 Change-Id: Ieee709ff15c13bdd0b1d7598e27031867062b592 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6443776 Reviewed-by: David Baron <[email protected]> Commit-Queue: Keith Cirkel <[email protected]> Commit-Queue: David Baron <[email protected]> Auto-Submit: Keith Cirkel <[email protected]> Cr-Commit-Position: refs/heads/main@{#1446589}
This implements the IDL definitions per the spec issue[1], however all methods are basic stubs and do not execute any logic - the logic will come with later patches. I2P: https://groups.google.com/a/chromium.org/g/blink-dev/c/um-9YjJWyEQ/m/MhKN0L7FAgAJ Spec PRs: HTML: whatwg/html#10869 DOM: whatwg/dom#1341 [1]: whatwg/html#10854 Bug: 409577162 Change-Id: Ieee709ff15c13bdd0b1d7598e27031867062b592 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6443776 Reviewed-by: David Baron <[email protected]> Commit-Queue: Keith Cirkel <[email protected]> Commit-Queue: David Baron <[email protected]> Auto-Submit: Keith Cirkel <[email protected]> Cr-Commit-Position: refs/heads/main@{#1446589}
This implements the IDL definitions per the spec issue[1], however all methods are basic stubs and do not execute any logic - the logic will come with later patches. I2P: https://groups.google.com/a/chromium.org/g/blink-dev/c/um-9YjJWyEQ/m/MhKN0L7FAgAJ Spec PRs: HTML: whatwg/html#10869 DOM: whatwg/dom#1341 [1]: whatwg/html#10854 Bug: 409577162 Change-Id: Ieee709ff15c13bdd0b1d7598e27031867062b592 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6443776 Reviewed-by: David Baron <[email protected]> Commit-Queue: Keith Cirkel <[email protected]> Commit-Queue: David Baron <[email protected]> Auto-Submit: Keith Cirkel <[email protected]> Cr-Commit-Position: refs/heads/main@{#1446589}
Is there a polyfill for the new Scoped Custom Element Registries yet? And if I use it, will I be shot? It is currently a bit difficult to search for information on Scoped Registries. The top result on Google, ![]() has not been updated in 5 years: ![]() @justinfagnani when you have a moment, or if someone else does, can we please link that to the latest spec? I don't know if it is just me, but anything that goes from its own spec space and into the |
FWIW, the latest scoped custom element registry API has been implemented and enabled in Safari Technology Preview. |
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 acustomElementRegistry
getter.shadowrootcustomelements
attribute, which is reflected as a string for forward compatibility.ElementInternals
gainsinitializeShadowRoot()
CustomElementRegistry
gainsinitialize()
so a declarative shadow root (or any element) can have itsCustomElementRegistry
set (when it’s null).attachShadow()
member is now calledcustomElementRegistry
for consistency.Element
should support an associatedCustomElementRegistry
, exposed through acustomElementRegistry
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.document.createElement()
,document.createElementNS()
, anddocument.importNode()
are updated to account for registries.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()
we should attempt to reuse the existing one ondocument
(with a newcustomElements
member) as we're not quite ready to reinvent how to best do element-creation.initializeSubtree()
perform upgrades in the connected case? (If yes, it should probably also update the scoped document set.) (No.)initializeSubtree()
? (Also consider the names already exposed on this object.) Just initialize.importNode()
is better after all as we need a document anyway. So we should go with adocument.importNode(node, { subtree, customElements })
overload.The text was updated successfully, but these errors were encountered: