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

refactor: simplify UpgradableConstructor use of native lifecycle callbacks #3862

Merged
merged 2 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion packages/@lwc/engine-core/src/framework/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export { freezeTemplate } from './freeze-template';
export { getComponentConstructor } from './get-component-constructor';

// Types -------------------------------------------------------------------------------------------
export type { RendererAPI, LifecycleCallback } from './renderer';
export type { RendererAPI, LifecycleCallback, LifecycleCallbacks } from './renderer';
export type {
ConfigValue as WireConfigValue,
ContextValue as WireContextValue,
Expand Down
26 changes: 11 additions & 15 deletions packages/@lwc/engine-core/src/framework/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ type N = HostNode;
type E = HostElement;

export type LifecycleCallback = (elm: E) => void;
export interface LifecycleCallbacks {
connectedCallback: LifecycleCallback;
disconnectedCallback: LifecycleCallback;
formAssociatedCallback: LifecycleCallback;
formDisabledCallback: LifecycleCallback;
formResetCallback: LifecycleCallback;
formStateRestoreCallback: LifecycleCallback;
}

export interface RendererAPI {
isSyntheticShadowDefined: boolean;
Expand Down Expand Up @@ -62,26 +70,14 @@ export interface RendererAPI {
isConnected: (node: N) => boolean;
insertStylesheet: (content: string, target?: ShadowRoot) => void;
assertInstanceOfHTMLElement: (elm: any, msg: string) => void;
createCustomElement: (
tagName: string,
upgradeCallback: LifecycleCallback,
connectedCallback?: LifecycleCallback,
disconnectedCallback?: LifecycleCallback,
formAssociatedCallback?: LifecycleCallback,
formDisabledCallback?: LifecycleCallback,
formResetCallback?: LifecycleCallback,
formStateRestoreCallback?: LifecycleCallback
) => E;
defineCustomElement: (
tagName: string,
connectedCallback?: LifecycleCallback,
disconnectedCallback?: LifecycleCallback
) => void;
createCustomElement: (tagName: string, upgradeCallback: LifecycleCallback) => E;
defineCustomElement: (tagName: string) => void;
ownerDocument(elm: E): Document;
registerContextConsumer: (
element: E,
adapterContextToken: string,
subscriptionPayload: WireContextSubscriptionPayload
) => void;
attachInternals: (elm: E) => ElementInternals;
setLifecycleCallbacks: (callbacks: LifecycleCallbacks) => void;
}
48 changes: 11 additions & 37 deletions packages/@lwc/engine-core/src/framework/rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {

import { logError } from '../shared/logger';
import { getComponentTag } from '../shared/format';
import { LifecycleCallback, RendererAPI } from './renderer';
import { RendererAPI } from './renderer';
import { EmptyArray } from './utils';
import { markComponentAsDirty } from './component';
import { getScopeTokenClass } from './stylesheet';
Expand Down Expand Up @@ -315,7 +315,7 @@ function mountCustomElement(
renderer: RendererAPI
) {
const { sel, owner } = vnode;
const { createCustomElement } = renderer;
const { createCustomElement, setLifecycleCallbacks } = renderer;
/**
* Note: if the upgradable constructor does not expect, or throw when we new it
* with a callback as the first argument, we could implement a more advanced
Expand All @@ -329,49 +329,23 @@ function mountCustomElement(
vm = createViewModelHook(elm, vnode, renderer);
};

let connectedCallback: LifecycleCallback | undefined;
let disconnectedCallback: LifecycleCallback | undefined;
let formAssociatedCallback: LifecycleCallback | undefined;
let formDisabledCallback: LifecycleCallback | undefined;
let formResetCallback: LifecycleCallback | undefined;
let formStateRestoreCallback: LifecycleCallback | undefined;

if (lwcRuntimeFlags.ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE) {
connectedCallback = (elm: HTMLElement) => {
connectRootElement(elm);
};
disconnectedCallback = (elm: HTMLElement) => {
disconnectRootElement(elm);
};
formAssociatedCallback = (elm: HTMLElement) => {
runFormAssociatedCallback(elm);
};
formDisabledCallback = (elm: HTMLElement) => {
runFormDisabledCallback(elm);
};
formResetCallback = (elm: HTMLElement) => {
runFormResetCallback(elm);
};
formStateRestoreCallback = (elm: HTMLElement) => {
runFormStateRestoreCallback(elm);
};
setLifecycleCallbacks({
jmsjtu marked this conversation as resolved.
Show resolved Hide resolved
connectedCallback: connectRootElement,
disconnectedCallback: disconnectRootElement,
formAssociatedCallback: runFormAssociatedCallback,
formDisabledCallback: runFormDisabledCallback,
formResetCallback: runFormResetCallback,
formStateRestoreCallback: runFormStateRestoreCallback,
});
}

// Should never get a tag with upper case letter at this point; the compiler
// should produce only tags with lowercase letters. However, the Java
// compiler may generate tagnames with uppercase letters so - for backwards
// compatibility, we lower case the tagname here.
const normalizedTagname = sel.toLowerCase();
const elm = createCustomElement(
normalizedTagname,
upgradeCallback,
connectedCallback,
disconnectedCallback,
formAssociatedCallback,
formDisabledCallback,
formResetCallback,
formStateRestoreCallback
);
const elm = createCustomElement(normalizedTagname, upgradeCallback);

vnode.elm = elm;
vnode.vm = vm;
Expand Down
47 changes: 10 additions & 37 deletions packages/@lwc/engine-dom/src/apis/create-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
connectRootElement,
disconnectRootElement,
LightningElement,
LifecycleCallback,
runFormAssociatedCallback,
runFormDisabledCallback,
runFormResetCallback,
Expand Down Expand Up @@ -109,7 +108,7 @@ export function createElement(
);
}

const { createCustomElement } = renderer;
const { createCustomElement, setLifecycleCallbacks } = renderer;

// tagName must be all lowercase, unfortunately, we have legacy code that is
// passing `sel` as a camel-case, which makes them invalid custom elements name
Expand All @@ -135,43 +134,17 @@ export function createElement(
}
};

let connectedCallback: LifecycleCallback | undefined;
let disconnectedCallback: LifecycleCallback | undefined;
let formAssociatedCallback: LifecycleCallback | undefined;
let formDisabledCallback: LifecycleCallback | undefined;
let formResetCallback: LifecycleCallback | undefined;
let formStateRestoreCallback: LifecycleCallback | undefined;

if (lwcRuntimeFlags.ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE) {
connectedCallback = (elm: HTMLElement) => {
connectRootElement(elm);
};
disconnectedCallback = (elm: HTMLElement) => {
disconnectRootElement(elm);
};
formAssociatedCallback = (elm: HTMLElement) => {
runFormAssociatedCallback(elm);
};
formDisabledCallback = (elm: HTMLElement) => {
runFormDisabledCallback(elm);
};
formResetCallback = (elm: HTMLElement) => {
runFormResetCallback(elm);
};
formStateRestoreCallback = (elm: HTMLElement) => {
runFormStateRestoreCallback(elm);
};
setLifecycleCallbacks({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a global setting, can we do this one time only instead of in each createElement invocation.

connectedCallback: connectRootElement,
disconnectedCallback: disconnectRootElement,
formAssociatedCallback: runFormAssociatedCallback,
formDisabledCallback: runFormDisabledCallback,
formResetCallback: runFormResetCallback,
formStateRestoreCallback: runFormStateRestoreCallback,
});
}

const element = createCustomElement(
tagName,
upgradeCallback,
connectedCallback,
disconnectedCallback,
formAssociatedCallback,
formDisabledCallback,
formResetCallback,
formStateRestoreCallback
);
const element = createCustomElement(tagName, upgradeCallback);
return element;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,22 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { isUndefined } from '@lwc/shared';
import { getLifecycleCallbacks } from '../lifecycle-callbacks';
import type { LifecycleCallback } from '@lwc/engine-core';

const cachedConstructors = new Map<string, CustomElementConstructor>();
const elementsUpgradedOutsideLWC = new WeakSet<HTMLElement>();
let elementBeingUpgradedByLWC = false;

let formAssociatedCallbackToUse: LifecycleCallback | undefined;
let formDisabledCallbackToUse: LifecycleCallback | undefined;
let formResetCallbackToUse: LifecycleCallback | undefined;
let formStateRestoreCallbackToUse: LifecycleCallback | undefined;

const instancesToFormAssociatedCallbacks = new WeakMap<HTMLElement, LifecycleCallback>();
const instancesToFormDisabledCallbacks = new WeakMap<HTMLElement, LifecycleCallback>();
const instancesToFormResetCallbacks = new WeakMap<HTMLElement, LifecycleCallback>();
const instancesToFormStateRestoreCallbacks = new WeakMap<HTMLElement, LifecycleCallback>();

// 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.
const createUpgradableConstructor = (
connectedCallback?: LifecycleCallback,
disconnectedCallback?: LifecycleCallback
) => {
const createUpgradableConstructor = () => {
const lifecycleCallbacks = getLifecycleCallbacks();
const connectedCallback = lifecycleCallbacks?.connectedCallback;
const disconnectedCallback = lifecycleCallbacks?.disconnectedCallback;
const hasConnectedCallback = !isUndefined(connectedCallback);
const hasDisconnectedCallback = !isUndefined(disconnectedCallback);

Expand All @@ -42,11 +33,6 @@ const createUpgradableConstructor = (
// If the element is not created using lwc.createElement(), e.g. `document.createElement('x-foo')`,
// then elementBeingUpgraded will be false
if (elementBeingUpgradedByLWC) {
instancesToFormAssociatedCallbacks.set(this, formAssociatedCallbackToUse!);
instancesToFormDisabledCallbacks.set(this, formDisabledCallbackToUse!);
instancesToFormResetCallbacks.set(this, formResetCallbackToUse!);
instancesToFormStateRestoreCallbacks.set(this, formStateRestoreCallbackToUse!);

upgradeCallback(this);
} else if (hasConnectedCallback || hasDisconnectedCallback) {
// If this element has connected or disconnected callbacks, then we need to keep track of
Expand All @@ -60,33 +46,33 @@ const createUpgradableConstructor = (
}

formAssociatedCallback() {
const formAssociatedCallback = instancesToFormAssociatedCallbacks.get(this);
const formAssociatedCallback = getLifecycleCallbacks()?.formAssociatedCallback;
// if element was upgraded outside LWC, this will be undefined
if (!isUndefined(formAssociatedCallback)) {
if (!isUndefined(formAssociatedCallback) && !elementsUpgradedOutsideLWC.has(this)) {
formAssociatedCallback(this);
}
}

formResetCallback() {
const formResetCallback = instancesToFormResetCallbacks.get(this);
const formResetCallback = getLifecycleCallbacks()?.formResetCallback;
// if element was upgraded outside LWC, this will be undefined
if (!isUndefined(formResetCallback)) {
if (!isUndefined(formResetCallback) && !elementsUpgradedOutsideLWC.has(this)) {
formResetCallback(this);
}
}

formDisabledCallback() {
const formDisabledCallback = instancesToFormDisabledCallbacks.get(this);
const formDisabledCallback = getLifecycleCallbacks()?.formDisabledCallback;
// if element was upgraded outside LWC, this will be undefined
if (!isUndefined(formDisabledCallback)) {
if (!isUndefined(formDisabledCallback) && !elementsUpgradedOutsideLWC.has(this)) {
formDisabledCallback(this);
}
}

formStateRestoreCallback() {
const formStateRestoreCallback = instancesToFormStateRestoreCallbacks.get(this);
const formStateRestoreCallback = getLifecycleCallbacks()?.formStateRestoreCallback;
// if element was upgraded outside LWC, this will be undefined
if (!isUndefined(formStateRestoreCallback)) {
if (!isUndefined(formStateRestoreCallback) && !elementsUpgradedOutsideLWC.has(this)) {
formStateRestoreCallback(this);
}
}
Expand All @@ -113,11 +99,7 @@ const createUpgradableConstructor = (
return UpgradableConstructor;
};

export function getUpgradableConstructor(
tagName: string,
connectedCallback?: LifecycleCallback,
disconnectedCallback?: LifecycleCallback
) {
export function getUpgradableConstructor(tagName: string) {
let UpgradableConstructor = cachedConstructors.get(tagName);

if (isUndefined(UpgradableConstructor)) {
Expand All @@ -126,45 +108,20 @@ export function getUpgradableConstructor(
`Unexpected tag name "${tagName}". This name is a registered custom element, preventing LWC to upgrade the element.`
);
}
UpgradableConstructor = createUpgradableConstructor(
connectedCallback,
disconnectedCallback
);
UpgradableConstructor = createUpgradableConstructor();
customElements.define(tagName, UpgradableConstructor);
cachedConstructors.set(tagName, UpgradableConstructor);
}
return UpgradableConstructor;
}

export const createCustomElement = (
tagName: string,
upgradeCallback: LifecycleCallback,
connectedCallback?: LifecycleCallback,
disconnectedCallback?: LifecycleCallback,
formAssociatedCallback?: LifecycleCallback,
formDisabledCallback?: LifecycleCallback,
formResetCallback?: LifecycleCallback,
formStateRestoreCallback?: LifecycleCallback
) => {
const UpgradableConstructor = getUpgradableConstructor(
tagName,
connectedCallback,
disconnectedCallback
);

formAssociatedCallbackToUse = formAssociatedCallback;
formDisabledCallbackToUse = formDisabledCallback;
formResetCallbackToUse = formResetCallback;
formStateRestoreCallbackToUse = formStateRestoreCallback;
export const createCustomElement = (tagName: string, upgradeCallback: LifecycleCallback) => {
const UpgradableConstructor = getUpgradableConstructor(tagName);

elementBeingUpgradedByLWC = true;
try {
return new UpgradableConstructor(upgradeCallback);
} finally {
elementBeingUpgradedByLWC = false;
formAssociatedCallbackToUse = undefined;
formDisabledCallbackToUse = undefined;
formResetCallbackToUse = undefined;
formStateRestoreCallbackToUse = undefined;
}
};
11 changes: 11 additions & 0 deletions packages/@lwc/engine-dom/src/lifecycle-callbacks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { LifecycleCallbacks } from '@lwc/engine-core';

let lifecycleCallbacks: LifecycleCallbacks | undefined;

export function setLifecycleCallbacks(callbacks: LifecycleCallbacks) {
lifecycleCallbacks = callbacks;
}

export function getLifecycleCallbacks(): LifecycleCallbacks | undefined {
return lifecycleCallbacks;
}
6 changes: 5 additions & 1 deletion packages/@lwc/engine-dom/src/renderer-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import type { RendererAPI } from '@lwc/engine-core';
// are omitted here
export type SandboxableRendererAPI = Omit<
RendererAPI,
'createCustomElement' | 'insertStylesheet' | 'isSyntheticShadowDefined' | 'defineCustomElement'
| 'createCustomElement'
| 'insertStylesheet'
| 'isSyntheticShadowDefined'
| 'defineCustomElement'
| 'setLifecycleCallbacks'
>;

export type RendererAPIType<Type> = Type extends RendererAPI ? RendererAPI : SandboxableRendererAPI;
Expand Down
3 changes: 2 additions & 1 deletion packages/@lwc/engine-dom/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
getUpgradableConstructor,
} from './custom-elements/create-custom-element';
import { rendererFactory } from './renderer-factory';

import { setLifecycleCallbacks } from './lifecycle-callbacks';
import type { RendererAPI } from '@lwc/engine-core';

/**
Expand All @@ -30,6 +30,7 @@ export const renderer: RendererAPI = assign(
// relies on a shared global cache
createCustomElement,
defineCustomElement: getUpgradableConstructor,
setLifecycleCallbacks,
isSyntheticShadowDefined: hasOwnProperty.call(Element.prototype, KEY__SHADOW_TOKEN),
}
);
Loading
Loading