Outdated - current proposal is at https://github.com/justinfagnani/scoped-custom-elements
Scoped Custom Element definitions is an oft-requested feature of Web Components. The global registry is a possible source of name collisions that may arise from coincidence, or from an app trying to define multiple versions of the same element, or from more advanced scenarios like registering mocks during tests, or a component explicitly replacing an element definition for its scope.
Since the key DOM creation APIs are global, scoping definitions is tricky because we'd need a machanis to determind which scope to use. But if we offer scoped versions of these APIs the problem is tractable. This requires that DOM creation code is upgraded to use the new scoped APIs, something that hopefully could be done in template libraries and frameworks.
This proposal adds the ability to construct CustomElementRegistry
s and chain them in order to inherit custom element definitions. It uses ShadowRoot
as a scope for definitions. ShadowRoot
can be associated with a CustomElementRegistry
when created and gains element creation methods, like createElement
. When new elements are created within a ShadowRoot
, that ShadowRoot
's registry is used to Custom Element upgrades.
-
CustomElementRegistry(parent?: CustomElementRegistry)
CustomElementRegistry
is constructable, and able to inherit from a parent registry.New definitions added to a registry are not visible to the parent, and mask any registrations with the same name defined in the parent so that definitions can be overridden.
-
CustomElementRegistry.prototype.get(name: string)
get()
now returns the closest constructor defined for a tag name in a chain of registries. -
CustomElementRegistry.prototype.getRegistry(name: string)
Returns the closest registry in which a tag name is defined.
ShadowRoot
s are already the scoping boundary for DOM and CSS, so it's natural to be the scope for custom elements. ShadowRoot
needs a CustomElementRegistry
and the DOM creation APIs that current exist on document.
-
customElements: CustomElementRegistry
The
CustomElementRegistry
theShadowRoot
uses, set onattachShadowRoot()
. -
createElement()
,createElementNS()
These methods create new elements using theCustomElementRegistry
of theShadowRoot
. -
importNode()
Imports a node into the document that owns theShadowRoot
, using theCustomElementRegistry
of theShadowRoot
.This enables cloning a template into multiple scopes to use different custom element definitions.
New properties:
-
Element.prototype.scope: Document | ShadowRoot
Elements have DOM creation APIs, likeinnerHTML
, so they need a reference to their scope. Elements expose this with ascope
property. One difference between this andgetRootNode()
is that the scope for an element can never change. -
Element.prototype.attachShadow(init: ShadowRootInit)
ShadowRootInit
adds a new property,customElements
, in its options argument which is aCustomElementRegistry
.
With a scope, DOM creation APIs like innerHTML
and insertAdjacentHTML
will use the element's scope
's registry to construct new custom elements. Appending or inserting an existing element doesn't use the scope, nor does it change the scope of the appended element. Scopes are completely defined when an element is created.
// x-foo.js is an existing custom element module that registers a class
// as 'x-foo' in the global registry.
import {XFoo} from './x-foo.js';
// Create a new registry that inherits from the global registry
const myRegistry = new CustomElementRegistry(window.customElements);
// Define a trivial subclass of XFoo so that we can register it ourselves
class MyFoo extends XFoo {}
// Register it as `my-foo` locally.
myRegistry.define('my-foo', MyFoo);
class MyElement extends HTMLElement {
constructor() {
super();
// Use the local registry when creating the ShadowRoot
this.attachShadow({mode: 'open', customElements: myRegistry});
// Use the scoped element creation APIs to create elements:
const myFoo = this.shadowRoot.createElement('my-foo');
this.shadowRoot.appendChild(myFoo);
// myFoo is now associated with the scope of `this.shadowRoot`, and registy
// of `myRegistry`. When it creates new DOM, is uses `myRegistry`:
myFoo.innerHTML = `<my-bar></my-bar>`;
}
}
-
What happens to existing upgraded elements when an overriding definition is added to a child registry?
The simplest answer is that elements are only ever upgraded once, and adding a new definition that's visible in an element's scope will not cause a re-upgrade or prototype change.
-
Should classes only be allow to be defined once, across all registries?
This would preserve the 1-1 relationship between a class and a tag name and the ability to do
new MyElement()
even if a class is not registered in the global registry.It's easy to define a trivial subclass if there's a need to register the same class in different registries or with different names.
-
Should registries inherit down the tree-of-trees by default, or only via the parent chain of
CustomElementRegistry
?Inheriting down the DOM tree leads to dynamic-like scoping where definitions can change depending on your position in the tree. Restricting to inheriting in
CustomElementRegistry
means there's a fixed lookup path. -
Should the registry of a
ShadowRoot
be final? -
Is
Element.prototype.scope
neccessary?It requires all elements to remember where they were created, possibly increasing their memory footprint. Scopes could be dynamically looked up during new DOM creation via the
getRootNode()
process instead, but this might slow down operations likeinnerHTML
. -
How does this interact with the Template Instantiation proposal?
With Template Instantiation
document.importNode()
isn't used to create template instances, butHTMLTemplateElement.prototype.createInstance()
. How will that know which scope to use? Should it take a registry orShadowRoot
?