-
Notifications
You must be signed in to change notification settings - Fork 393
/
create-custom-element.ts
144 lines (131 loc) · 6.34 KB
/
create-custom-element.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/*
* Copyright (c) 2023, Salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { isUndefined, isTrue } from '@lwc/shared';
import {
LifecycleCallback,
connectRootElement,
disconnectRootElement,
runFormAssociatedCallback,
runFormDisabledCallback,
runFormResetCallback,
runFormStateRestoreCallback,
FormRestoreState,
FormRestoreReason,
} from '@lwc/engine-core';
const cachedConstructors = new Map<string, CustomElementConstructor>();
const nativeLifecycleElementsToUpgradedByLWC = new WeakMap<HTMLElement, boolean>();
let elementBeingUpgradedByLWC = false;
let BaseUpgradableConstructor: CustomElementConstructor | undefined;
let BaseHTMLElement: typeof HTMLElement | undefined;
function createBaseUpgradableConstructor() {
// Creates a constructor that is intended to be used directly as a custom element, except that the upgradeCallback is
// passed in to the constructor so LWC can reuse the same custom element constructor for multiple components.
// Another benefit is that only LWC can create components that actually do anything – if you do
// `customElements.define('x-foo')`, then you don't have access to the upgradeCallback, so it's a dummy custom element.
// This class should be created once per tag name.
// TODO [#2972]: this class should expose observedAttributes as necessary
BaseUpgradableConstructor = class TheBaseUpgradableConstructor extends HTMLElement {
constructor(upgradeCallback: LifecycleCallback, useNativeLifecycle: boolean) {
super();
if (useNativeLifecycle) {
// When in native lifecycle mode, we need to keep track of instances that were created outside LWC
// (i.e. not created by `lwc.createElement()`). If the element uses synthetic lifecycle, then we don't
// need to track this.
nativeLifecycleElementsToUpgradedByLWC.set(this, elementBeingUpgradedByLWC);
}
// If the element is not created using lwc.createElement(), e.g. `document.createElement('x-foo')`,
// then elementBeingUpgradedByLWC will be false
if (elementBeingUpgradedByLWC) {
upgradeCallback(this);
}
// TODO [#2970]: LWC elements cannot be upgraded via new Ctor()
// Do we want to support this? Throw an error? Currently for backwards compat it's a no-op.
}
connectedCallback() {
if (isTrue(nativeLifecycleElementsToUpgradedByLWC.get(this))) {
connectRootElement(this);
}
}
disconnectedCallback() {
if (isTrue(nativeLifecycleElementsToUpgradedByLWC.get(this))) {
disconnectRootElement(this);
}
}
formAssociatedCallback(form: HTMLFormElement | null) {
if (isTrue(nativeLifecycleElementsToUpgradedByLWC.get(this))) {
runFormAssociatedCallback(this, form);
}
}
formDisabledCallback(disabled: boolean) {
if (isTrue(nativeLifecycleElementsToUpgradedByLWC.get(this))) {
runFormDisabledCallback(this, disabled);
}
}
formResetCallback() {
if (isTrue(nativeLifecycleElementsToUpgradedByLWC.get(this))) {
runFormResetCallback(this);
}
}
formStateRestoreCallback(state: FormRestoreState | null, reason: FormRestoreReason) {
if (isTrue(nativeLifecycleElementsToUpgradedByLWC.get(this))) {
runFormStateRestoreCallback(this, state, reason);
}
}
};
BaseHTMLElement = HTMLElement; // cache to track if it changes
}
const createUpgradableConstructor = (isFormAssociated: boolean) => {
if (HTMLElement !== BaseHTMLElement) {
// If the global HTMLElement changes out from under our feet, then we need to create a new
// BaseUpgradableConstructor from scratch (since it extends from HTMLElement). This can occur if
// polyfills are in play, e.g. a polyfill for scoped custom element registries.
// This workaround can potentially be removed when W-15361244 is resolved.
createBaseUpgradableConstructor();
}
// Using a BaseUpgradableConstructor superclass here is a perf optimization to avoid
// re-defining the same logic (connectedCallback, disconnectedCallback, etc.) over and over.
class UpgradableConstructor extends (BaseUpgradableConstructor!) {}
if (isFormAssociated) {
// Perf optimization - the vast majority of components have formAssociated=false,
// so we can skip the setter in those cases, since undefined works the same as false.
UpgradableConstructor.formAssociated = isFormAssociated;
}
return UpgradableConstructor;
};
export function getUpgradableConstructor(tagName: string, isFormAssociated: boolean) {
let UpgradableConstructor = cachedConstructors.get(tagName);
if (isUndefined(UpgradableConstructor)) {
if (!isUndefined(customElements.get(tagName))) {
throw new Error(
`Unexpected tag name "${tagName}". This name is a registered custom element, preventing LWC to upgrade the element.`
);
}
UpgradableConstructor = createUpgradableConstructor(isFormAssociated);
customElements.define(tagName, UpgradableConstructor);
cachedConstructors.set(tagName, UpgradableConstructor);
}
return UpgradableConstructor;
}
export const createCustomElement = (
tagName: string,
upgradeCallback: LifecycleCallback,
useNativeLifecycle: boolean,
isFormAssociated: boolean
) => {
const UpgradableConstructor = getUpgradableConstructor(tagName, isFormAssociated);
if (Boolean(UpgradableConstructor.formAssociated) !== isFormAssociated) {
throw new Error(
`<${tagName}> was already registered with formAssociated=${UpgradableConstructor.formAssociated}. It cannot be re-registered with formAssociated=${isFormAssociated}. Please rename your component to have a different name than <${tagName}>`
);
}
elementBeingUpgradedByLWC = true;
try {
return new UpgradableConstructor(upgradeCallback, useNativeLifecycle);
} finally {
elementBeingUpgradedByLWC = false;
}
};