Skip to content

Commit 5658304

Browse files
author
Steve Orvell
committed
Update to sport revamped proposal in whatwg/html#10854
1 parent 10f8e07 commit 5658304

File tree

5 files changed

+319
-189
lines changed

5 files changed

+319
-189
lines changed

packages/scoped-custom-element-registry/src/scoped-custom-element-registry.ts

+147-57
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ interface CustomHTMLElement {
7676

7777
interface CustomElementRegistry {
7878
_getDefinition(tagName: string): CustomElementDefinition | undefined;
79+
createElement(tagName: string): Node;
80+
cloneSubtree(node: Node): Node;
7981
}
8082

8183
interface CustomElementDefinition {
@@ -106,13 +108,13 @@ interface CustomElementDefinition {
106108
// Note, `registry` matches proposal but `customElements` was previously
107109
// proposed. It's supported for back compat.
108110
interface ShadowRootWithSettableCustomElements extends ShadowRoot {
109-
registry?: CustomElementRegistry;
110-
customElements?: CustomElementRegistry;
111+
registry?: CustomElementRegistry | null;
112+
customElements: CustomElementRegistry | null;
111113
}
112114

113115
interface ShadowRootInitWithSettableCustomElements extends ShadowRootInit {
114-
registry?: CustomElementRegistry;
115-
customElements?: CustomElementRegistry;
116+
registry?: CustomElementRegistry | null;
117+
customElements?: CustomElementRegistry | null;
116118
}
117119

118120
type ParametersOf<
@@ -137,12 +139,29 @@ const globalDefinitionForConstructor = new WeakMap<
137139
CustomElementConstructor,
138140
CustomElementDefinition
139141
>();
140-
// TBD: This part of the spec proposal is unclear:
141-
// > Another option for looking up registries is to store an element's
142-
// > originating registry with the element. The Chrome DOM team was concerned
143-
// > about the small additional memory overhead on all elements. Looking up the
144-
// > root avoids this.
145-
const scopeForElement = new WeakMap<Node, Element | ShadowRoot>();
142+
143+
const registryForElement = new WeakMap<
144+
Node,
145+
ShimmedCustomElementsRegistry | null
146+
>();
147+
const registryToSubtree = (
148+
node: Node,
149+
registry: ShimmedCustomElementsRegistry | null,
150+
shouldUpgrade?: boolean
151+
) => {
152+
if (registryForElement.get(node) == null) {
153+
registryForElement.set(node, registry);
154+
}
155+
if (shouldUpgrade && registryForElement.get(node) === registry) {
156+
registry?._upgradeElement(node as HTMLElement);
157+
}
158+
const {children} = node as Element;
159+
if (children?.length) {
160+
Array.from(children).forEach((child) =>
161+
registryToSubtree(child, registry, shouldUpgrade)
162+
);
163+
}
164+
};
146165

147166
class AsyncInfo<T> {
148167
readonly promise: Promise<T>;
@@ -251,8 +270,7 @@ class ShimmedCustomElementsRegistry implements CustomElementRegistry {
251270
if (awaiting) {
252271
this._awaitingUpgrade.delete(tagName);
253272
for (const element of awaiting) {
254-
pendingRegistryForElement.delete(element);
255-
customize(element, definition, true);
273+
this._upgradeElement(element, definition);
256274
}
257275
}
258276
// Flush whenDefined callbacks
@@ -268,6 +286,7 @@ class ShimmedCustomElementsRegistry implements CustomElementRegistry {
268286
creationContext.push(this);
269287
nativeRegistry.upgrade(...args);
270288
creationContext.pop();
289+
args.forEach((n) => registryToSubtree(n, this));
271290
}
272291

273292
get(tagName: string) {
@@ -312,6 +331,39 @@ class ShimmedCustomElementsRegistry implements CustomElementRegistry {
312331
awaiting.delete(element);
313332
}
314333
}
334+
335+
// upgrades the given element if defined or queues it for upgrade when defined.
336+
_upgradeElement(element: HTMLElement, definition?: CustomElementDefinition) {
337+
definition ??= this._getDefinition(element.localName);
338+
if (definition !== undefined) {
339+
pendingRegistryForElement.delete(element);
340+
customize(element, definition!, true);
341+
} else {
342+
this._upgradeWhenDefined(element, element.localName, true);
343+
}
344+
}
345+
346+
['createElement'](localName: string) {
347+
creationContext.push(this);
348+
const el = document.createElement(localName);
349+
creationContext.pop();
350+
registryToSubtree(el, this);
351+
return el;
352+
}
353+
354+
['cloneSubtree'](node: Node) {
355+
creationContext.push(this);
356+
// Note, cannot use `cloneNode` here becuase the node may not be in this document
357+
const subtree = document.importNode(node, true);
358+
creationContext.pop();
359+
registryToSubtree(subtree, this);
360+
return subtree;
361+
}
362+
363+
['initializeSubtree'](node: Node) {
364+
registryToSubtree(node, this, true);
365+
return node;
366+
}
315367
}
316368

317369
// User extends this HTMLElement, which returns the CE being upgraded
@@ -345,35 +397,23 @@ window.HTMLElement = (function HTMLElement(this: HTMLElement) {
345397
window.HTMLElement.prototype = NativeHTMLElement.prototype;
346398

347399
// Helpers to return the scope for a node where its registry would be located
348-
const isValidScope = (node: Node) =>
349-
node === document || node instanceof ShadowRoot;
400+
// const isValidScope = (node: Node) =>
401+
// node === document || node instanceof ShadowRoot;
350402
const registryForNode = (node: Node): ShimmedCustomElementsRegistry | null => {
351-
// TODO: the algorithm for finding the scope is a bit up in the air; assigning
352-
// a one-time scope at creation time would require walking every tree ever
353-
// created, which is avoided for now
354-
let scope = node.getRootNode();
355-
// If we're not attached to the document (i.e. in a disconnected tree or
356-
// fragment), we need to get the scope from the creation context; that should
357-
// be a Document or ShadowRoot, unless it was created via innerHTML
358-
if (!isValidScope(scope)) {
359-
const context = creationContext[creationContext.length - 1];
360-
// When upgrading via registry.upgrade(), the registry itself is put on the
361-
// creationContext stack
362-
if (context instanceof CustomElementRegistry) {
363-
return context as ShimmedCustomElementsRegistry;
364-
}
365-
// Otherwise, get the root node of the element this was created from
366-
scope = context.getRootNode();
367-
// The creation context wasn't a Document or ShadowRoot or in one; this
368-
// means we're being innerHTML'ed into a disconnected element; for now, we
369-
// hope that root node was created imperatively, where we stash _its_
370-
// scopeForElement. Beyond that, we'd need more costly tracking.
371-
if (!isValidScope(scope)) {
372-
scope = scopeForElement.get(scope)?.getRootNode() || document;
373-
}
403+
const context = creationContext[creationContext.length - 1];
404+
if (context instanceof CustomElementRegistry) {
405+
return context as ShimmedCustomElementsRegistry;
374406
}
375-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
376-
return (scope as any)['registry'] as ShimmedCustomElementsRegistry | null;
407+
if (
408+
context?.nodeType === Node.ELEMENT_NODE ||
409+
context?.nodeType === Node.DOCUMENT_FRAGMENT_NODE
410+
) {
411+
return context.customElements as ShimmedCustomElementsRegistry;
412+
}
413+
return node.nodeType === Node.ELEMENT_NODE
414+
? ((node as Element).customElements as ShimmedCustomElementsRegistry) ??
415+
null
416+
: null;
377417
};
378418

379419
// Helper to create stand-in element for each tagName registered that delegates
@@ -400,13 +440,11 @@ const createStandInElement = (tagName: string): CustomElementConstructor => {
400440
// upgrade will eventually install the full CE prototype
401441
Object.setPrototypeOf(instance, HTMLElement.prototype);
402442
// Get the node's scope, and its registry (falls back to global registry)
403-
const registry =
404-
registryForNode(instance) ||
405-
(window.customElements as ShimmedCustomElementsRegistry);
406-
const definition = registry._getDefinition(tagName);
443+
const registry = registryForNode(instance);
444+
const definition = registry?._getDefinition(tagName);
407445
if (definition) {
408446
customize(instance, definition);
409-
} else {
447+
} else if (registry) {
410448
pendingRegistryForElement.set(instance, registry);
411449
}
412450
return instance;
@@ -423,10 +461,25 @@ const createStandInElement = (tagName: string): CustomElementConstructor => {
423461
definition.connectedCallback &&
424462
definition.connectedCallback.apply(this, args);
425463
} else {
464+
// NOTE, if this has a null registry, then it should be changed
465+
// to the registry into which it's inserted.
466+
// LIMITATION: this is only done for custom elements and not built-ins
467+
// since we can't easily see their connection state changing.
426468
// Register for upgrade when defined (only when connected, so we don't leak)
427-
pendingRegistryForElement
428-
.get(this)!
429-
._upgradeWhenDefined(this, tagName, true);
469+
const pendingRegistry = pendingRegistryForElement.get(this);
470+
if (pendingRegistry !== undefined) {
471+
pendingRegistry._upgradeWhenDefined(this, tagName, true);
472+
} else {
473+
const registry =
474+
this.customElements ?? this.parentElement?.customElements;
475+
if (registry) {
476+
registryToSubtree(
477+
this,
478+
registry as ShimmedCustomElementsRegistry,
479+
true
480+
);
481+
}
482+
}
430483
}
431484
}
432485

@@ -677,15 +730,51 @@ Element.prototype.attachShadow = function (
677730
...args,
678731
] as unknown) as [init: ShadowRootInit];
679732
const shadowRoot = nativeAttachShadow.apply(this, nativeArgs);
680-
const registry = init['registry'] ?? init.customElements;
733+
// Note, this allows a `null` customElements purely for testing.
734+
const registry =
735+
init['customElements'] === undefined
736+
? init['registry']
737+
: init['customElements'];
681738
if (registry !== undefined) {
682-
(shadowRoot as ShadowRootWithSettableCustomElements).customElements = (shadowRoot as ShadowRootWithSettableCustomElements)[
683-
'registry'
684-
] = registry;
739+
registryForElement.set(
740+
shadowRoot,
741+
registry as ShimmedCustomElementsRegistry
742+
);
743+
(shadowRoot as ShadowRootWithSettableCustomElements)['registry'] = registry;
685744
}
686745
return shadowRoot;
687746
};
688747

748+
const customElementsDescriptor = {
749+
get(this: Element) {
750+
const registry = registryForElement.get(this);
751+
return registry === undefined
752+
? ((this.nodeType === Node.DOCUMENT_NODE
753+
? this
754+
: this.ownerDocument) as Document)?.defaultView?.customElements ||
755+
null
756+
: registry;
757+
},
758+
enumerable: true,
759+
configurable: true,
760+
};
761+
762+
Object.defineProperty(
763+
Element.prototype,
764+
'customElements',
765+
customElementsDescriptor
766+
);
767+
Object.defineProperty(
768+
Document.prototype,
769+
'customElements',
770+
customElementsDescriptor
771+
);
772+
Object.defineProperty(
773+
ShadowRoot.prototype,
774+
'customElements',
775+
customElementsDescriptor
776+
);
777+
689778
// Install scoped creation API on Element & ShadowRoot
690779
const creationContext: Array<
691780
Document | CustomElementRegistry | Element | ShadowRoot
@@ -707,15 +796,15 @@ const installScopedCreationMethod = (
707796
// insertAdjacentHTML doesn't return an element, but that's fine since
708797
// it will have a parent that should have a scope
709798
if (ret !== undefined) {
710-
scopeForElement.set(ret, this);
799+
registryToSubtree(
800+
ret,
801+
this.customElements as ShimmedCustomElementsRegistry
802+
);
711803
}
712804
creationContext.pop();
713805
return ret;
714806
};
715807
};
716-
installScopedCreationMethod(ShadowRoot, 'createElement', document);
717-
installScopedCreationMethod(ShadowRoot, 'createElementNS', document);
718-
installScopedCreationMethod(ShadowRoot, 'importNode', document);
719808
installScopedCreationMethod(Element, 'insertAdjacentHTML');
720809

721810
// Install scoped innerHTML on Element & ShadowRoot
@@ -727,6 +816,7 @@ const installScopedCreationSetter = (ctor: Function, name: string) => {
727816
creationContext.push(this);
728817
descriptor.set!.call(this, value);
729818
creationContext.pop();
819+
registryToSubtree(this, this.customElements);
730820
},
731821
});
732822
};
@@ -759,10 +849,10 @@ if (
759849
return internals;
760850
};
761851

852+
const proto = window['ElementInternals'].prototype;
853+
762854
methods.forEach((method) => {
763-
const proto = window['ElementInternals'].prototype;
764855
const originalMethod = proto[method] as Function;
765-
766856
// eslint-disable-next-line @typescript-eslint/no-explicit-any
767857
(proto as any)[method] = function (...args: Array<unknown>) {
768858
const host = internalsToHostMap.get(this);

packages/scoped-custom-element-registry/src/types.d.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export {};
22

33
declare global {
4-
interface ShadowRoot {
4+
interface CustomElementRegistry {
55
// This overload is for roots that use the global registry
66
createElement<K extends keyof HTMLElementTagNameMap>(
77
tagName: K,
@@ -16,14 +16,28 @@ declare global {
1616
tagName: string,
1717
options?: ElementCreationOptions
1818
): HTMLElement;
19+
cloneSubtree(node: Node): Node;
20+
initializeSubtree: (node: Node) => Node;
1921
}
2022

2123
interface ShadowRootInit {
22-
customElements?: CustomElementRegistry;
24+
customElements?: CustomElementRegistry | null;
2325
}
2426

2527
interface ShadowRoot {
26-
readonly customElements?: CustomElementRegistry;
28+
readonly customElements: CustomElementRegistry | null;
29+
}
30+
31+
interface Document {
32+
readonly customElements: CustomElementRegistry | null;
33+
}
34+
35+
interface Element {
36+
readonly customElements: CustomElementRegistry | null;
37+
}
38+
39+
interface InitializeShadowRootInit {
40+
customElements?: CustomElementRegistry;
2741
}
2842

2943
/*

0 commit comments

Comments
 (0)