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

[scoped-registries] Describe interaction with declarative shadow DOM #915

Open
wants to merge 3 commits into
base: gh-pages
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 60 additions & 11 deletions proposals/Scoped-Custom-Element-Registries.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ and associate them with a ShadowRoot:
```js
export class MyElement extends HTMLElement {
constructor() {
this.attachShadow({mode: 'open', registry});
this.attachShadow({mode: 'open', customElements: registry});
}
}
```
Expand All @@ -70,7 +70,7 @@ These scoped registries will allow for different parts of a page to contain defi

### Creating and using a `CustomElementRegistry`

A new `CustomElementRegistry` is created with the `CustomElementRegistry` constructor, and attached to a ShadowRoot with the `registry` option to `HTMLElement.prototype.attachShadow`:
A new `CustomElementRegistry` is created with the `CustomElementRegistry` constructor, and attached to a ShadowRoot with the `customElements` option to `HTMLElement.prototype.attachShadow()`:

```js
import {OtherElement} from './my-element.js';
Expand All @@ -80,7 +80,7 @@ registry.define('other-element', OtherElement);

export class MyElement extends HTMLElement {
constructor() {
this.attachShadow({mode: 'open', registry});
this.attachShadow({mode: 'open', customElements: registry});
}
}
```
Expand All @@ -107,7 +107,7 @@ The context node is the node that hosts the element creation API that was invoke

#### Note on looking up registries

One consequence of looking up a registry from the root at element creation time is that different registries could be used over time for some APIs like HTMLElement.prototype.innerHTML, if the context node moves between shadow roots. This should be exceedingly rare though.
One consequence of looking up a registry from the root at element creation time is that different registries could be used over time for some APIs like `HTMLElement.prototype.innerHTML`, if the context node moves between shadow roots. This should be exceedingly rare though.

Another option for looking up registries is to store an element's originating registry with the element. The Chrome DOM team was concerned about the small additional memory overhead on all elements. Looking up the root avoids this.

Expand All @@ -119,6 +119,51 @@ As a result, it must limit constructors by default to only looking up registrati

This poses a limitation for authors trying to use the constructor to create new elements associated to scoped registries but not registered as global. More flexibility can be analyzed post MVP, for now, a user-land abstraction can help by keeping track of the constructor and its respective registry.

## Interaction with Declarative Shadow DOM

[Declarative shadow DOM](https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md) allows HTML to construct shadow roots for elements from `<template>` elements with a `shadowrootmode` attribute.

Since these shadow roots are not created by a host calling `attachShadow()`, the host doesn't have a chance to pass in a scoped custom element registry. If a host is using a scoped registry, we need to force the declarative shadow root to not use the global registry and instead leave custom elements un-upgraded until the host can create and assign the correct registry.

To do this we add a `shadowrootregistry` attribute, according to the [declarative shadow root explainer section on additional `attachShadow()` options](https://github.com/mfreed7/declarative-shadow-dom/blob/master/README.md#additional-arguments-for-attachshadow). This attribute causes the declarative shadow root to have no registry associated with it at all:

```html
<my-element>
<template shadowrootmode="open" shadowrootregistry="">
<some-scoped-element></some-scoped-element>
</template>
</my-element>
```

The shadow root created by this HTML will have a `null` registry, and be in a "awaiting scoped registry" state that allows the registry to be set once after creation.

To identify this "no registry, but awaiting one" state, ShadowRoot will have a `hasScopedRegistry` boolean property. `hasScopedRegistry` will set to `true` for all ShadowRoots with a scoped registry, or awaiting a scoped registry. Code can tell that ShadowRoot has an assignable `customElements` property if `root.customElements === null && root.hasScopedRegistry === true`.

Host elements with declarative scoped registries can assign the correct registry during upgrades, like so:

```ts
const registry = new CustomElementRegistry();

class MyElement extends HTMLElement {
#internals = null;
constructor() {
super();
this.#internals = this.attachInternals();
let shadowRoot = this.#internals.shadowRoot;
if (shadowRoot !== null) {
if (shadowRoot.customElements === null &&
shadowRoot.hasScopedRegistry) {
this.#internals.shadowRoot.customElements = registry;
} else {
console.error(`Expected shadowRoot.hasScopedRegistry to be true`);
}
} else {
this.attachShadow({mode: 'open', registry});
}
}
}
```

## API

### CustomElementRegistry
Expand All @@ -133,6 +178,12 @@ The `CustomElementRegistry` constructor creates a new instance of CustomElementR
const registry = new CustomElementRegistry();
```

### ShadowRootInit

ShadowRootInit adds the `customElements` property:

* `customElements`: `CustomElementRegistry | null | undefined`

### ShadowRoot

ShadowRoot adds element creation APIs that were previously only available on Document:
Expand All @@ -143,6 +194,11 @@ ShadowRoot adds element creation APIs that were previously only available on Doc

These are added to provide a root and possible registry to look up a custom element definition.

ShadowRoot also adds `customElements`, and `hasScopedRegistry` properties:

* `customElements`: `CustomElementRegistry | null`
* `hasScopedRegistry`: `boolean`

## Open Questions

### Adopted elements and Scoped Registry
Expand All @@ -154,13 +210,6 @@ There are concern about what happens when an element with a custom registry move
3. create a new callback that can receive the new registry when moved. This is problematic as well because what happen when the registries are coming from another library, already created by someone else?
4. find ways for implementers to preserve the original registry (ideal).

### Intersection with Declarative Shadow Root

Although these two proposals are in early stages, we need to solve the intersection semantics. There are two main issues:

1. if a declarative shadow root is created, elements inside that shadow should not be upgraded with the global registry. a possible solution is to add a new attribute, similar to `mode` to indicate to the parser that a custom registry is going to be eventually associated to this shadow.
2. if the component is planning to reuse the instance of the declarative shadow root (which is ideal), how can the component associate that instance with a registry? this indicates that maybe the association between ShadowRoot and CustomElementRegistry cannot be defined via `attachShadow()`, and instead, something like `ElementInternals` is much more flexible.

## Alternatives to allowing multiple definitions

### More robust registration patterns
Expand Down