Skip to content

Commit

Permalink
feat(versioning): use native custom element lifecycle
Browse files Browse the repository at this point in the history
  • Loading branch information
nolanlawson committed Apr 5, 2023
1 parent 375f23e commit 670593f
Show file tree
Hide file tree
Showing 36 changed files with 382 additions and 128 deletions.
14 changes: 7 additions & 7 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ commands:
force_native_shadow_mode:
type: boolean
default: false
enable_native_custom_element_lifecycle:
api_version_59:
type: boolean
default: false
default: true
disable_aria_reflection_polyfill:
type: boolean
default: false
Expand Down Expand Up @@ -86,9 +86,9 @@ commands:
exit 1
fi
}
<<# parameters.api_version_59 >> API_VERSION=59 <</ parameters.api_version_59 >> \
<<# parameters.disable_synthetic >> DISABLE_SYNTHETIC=1 <</ parameters.disable_synthetic >> \
<<# parameters.force_native_shadow_mode >> FORCE_NATIVE_SHADOW_MODE_FOR_TEST=1 <</ parameters.force_native_shadow_mode >> \
<<# parameters.enable_native_custom_element_lifecycle >> ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE=1 <</ parameters.enable_native_custom_element_lifecycle >> \
<<# parameters.disable_aria_reflection_polyfill >> DISABLE_ARIA_REFLECTION_POLYFILL=1 <</ parameters.disable_aria_reflection_polyfill >> \
<<# parameters.node_env_production >> NODE_ENV_FOR_TEST=production <</ parameters.node_env_production >> \
<<# parameters.disable_synthetic_shadow_support_in_compiler >> DISABLE_SYNTHETIC_SHADOW_SUPPORT_IN_COMPILER=1 <</ parameters.disable_synthetic_shadow_support_in_compiler >> \
Expand Down Expand Up @@ -176,7 +176,7 @@ commands:
force_native_shadow_mode:
type: boolean
default: false
enable_native_custom_element_lifecycle:
api_version_59:
type: boolean
default: false
disable_aria_reflection_polyfill:
Expand All @@ -200,7 +200,7 @@ commands:
working_directory: packages/@lwc/integration-karma
disable_synthetic: << parameters.disable_synthetic >>
force_native_shadow_mode: << parameters.force_native_shadow_mode >>
enable_native_custom_element_lifecycle: << parameters.enable_native_custom_element_lifecycle >>
api_version_59: << parameters.api_version_59 >>
disable_aria_reflection_polyfill: << parameters.disable_aria_reflection_polyfill >>
node_env_production: << parameters.node_env_production >>
disable_synthetic_shadow_support_in_compiler: << parameters.disable_synthetic_shadow_support_in_compiler >>
Expand Down Expand Up @@ -262,10 +262,10 @@ jobs:
- run_karma:
force_native_shadow_mode: true
- run_karma:
enable_native_custom_element_lifecycle: true
api_version_59: true
- run_karma:
disable_synthetic: true
enable_native_custom_element_lifecycle: true
api_version_59: true
- run_karma:
disable_synthetic: true
disable_aria_reflection_polyfill: true
Expand Down
4 changes: 2 additions & 2 deletions packages/@lwc/engine-core/src/framework/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export interface RendererAPI {
createCustomElement: (
tagName: string,
upgradeCallback: LifecycleCallback,
connectedCallback?: LifecycleCallback,
disconnectedCallback?: LifecycleCallback
connectedCallback: LifecycleCallback,
disconnectedCallback: LifecycleCallback
) => E;
ownerDocument(elm: E): Document;
registerContextConsumer: (
Expand Down
40 changes: 27 additions & 13 deletions packages/@lwc/engine-core/src/framework/rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import {
ArrayPush,
APIFeature,
ArrayPop,
ArrayPush,
ArraySome,
assert,
create,
isAPIFeatureEnabled,
isArray,
isFalse,
isNull,
Expand All @@ -23,9 +25,9 @@ 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 { getComponentAPIVersion, markComponentAsDirty } from './component';
import { getScopeTokenClass } from './stylesheet';
import { lockDomMutation, patchElementWithRestrictions, unlockDomMutation } from './restrictions';
import {
Expand Down Expand Up @@ -70,6 +72,7 @@ import { patchStyleAttribute } from './modules/computed-style-attr';
import { applyEventListeners } from './modules/events';
import { applyStaticClassAttribute } from './modules/static-class-attr';
import { applyStaticStyleAttribute } from './modules/static-style-attr';
import { LightningElementConstructor } from './base-lightning-element';

export function patchChildren(
c1: VNodes,
Expand Down Expand Up @@ -295,6 +298,18 @@ function mountStatic(
insertNode(elm, parent, anchor, renderer);
}

function shouldVMUseNativeCustomElementLifecycle(vm: VM | undefined) {
if (isUndefined(vm)) {
return false;
}

const apiVersion = getComponentAPIVersion(
vm.component.constructor as LightningElementConstructor
);

return isAPIFeatureEnabled(APIFeature.ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE, apiVersion);
}

function mountCustomElement(
vnode: VCustomElement,
parent: ParentNode,
Expand All @@ -316,17 +331,16 @@ function mountCustomElement(
vm = createViewModelHook(elm, vnode, renderer);
};

let connectedCallback: LifecycleCallback | undefined;
let disconnectedCallback: LifecycleCallback | undefined;

if (lwcRuntimeFlags.ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE) {
connectedCallback = (elm: HTMLElement) => {
const connectedCallback = (elm: HTMLElement) => {
if (shouldVMUseNativeCustomElementLifecycle(vm)) {
connectRootElement(elm);
};
disconnectedCallback = (elm: HTMLElement) => {
}
};
const disconnectedCallback = (elm: HTMLElement) => {
if (shouldVMUseNativeCustomElementLifecycle(vm)) {
disconnectRootElement(elm);
};
}
}
};

// Should never get a tag with upper case letter at this point; the compiler
// should produce only tags with lowercase letters. However, the Java
Expand Down Expand Up @@ -355,7 +369,7 @@ function mountCustomElement(

if (vm) {
if (process.env.IS_BROWSER) {
if (!lwcRuntimeFlags.ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE) {
if (!shouldVMUseNativeCustomElementLifecycle(vm)) {
if (process.env.NODE_ENV !== 'production') {
// With synthetic lifecycle callbacks, it's possible for elements to be removed without the engine
// noticing it (e.g. `appendChild` the same host element twice). This test ensures we don't regress.
Expand Down
45 changes: 31 additions & 14 deletions packages/@lwc/engine-dom/src/apis/create-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ import {
isNull,
isObject,
isUndefined,
toString,
StringToLowerCase,
toString,
APIFeature,
isAPIFeatureEnabled,
} from '@lwc/shared';
import {
createVM,
connectRootElement,
createVM,
disconnectRootElement,
LightningElement,
LifecycleCallback,
getComponentAPIVersion,
} from '@lwc/engine-core';
import { renderer } from '../renderer';

Expand All @@ -46,7 +48,14 @@ function callNodeSlot(node: Node, slot: WeakMap<any, NodeSlotCallback>): Node {
return node; // for convenience
}

if (!lwcRuntimeFlags.ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE) {
let monkeyPatched = false;

function monkeyPatchDomAPIs() {
if (monkeyPatched) {
// don't double-patch
return;
}
monkeyPatched = true;
// Monkey patching Node methods to be able to detect the insertions and removal of root elements
// created via createElement.
const { appendChild, insertBefore, removeChild, replaceChild } = _Node.prototype;
Expand Down Expand Up @@ -112,6 +121,12 @@ export function createElement(
// the following line guarantees that this does not leaks beyond this point.
const tagName = StringToLowerCase.call(sel);

const apiVersion = getComponentAPIVersion(Ctor);
const useNativeCustomElementLifecycle = isAPIFeatureEnabled(
APIFeature.ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE,
apiVersion
);

// the custom element from the registry is expecting an upgrade callback
/**
* Note: if the upgradable constructor does not expect, or throw when we new it
Expand All @@ -125,23 +140,25 @@ export function createElement(
mode: options.mode !== 'closed' ? 'open' : 'closed',
owner: null,
});
if (!lwcRuntimeFlags.ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE) {
if (!useNativeCustomElementLifecycle) {
// Monkey-patch on-demand, because if there are no components on the page using an old API
// version, then we don't want to monkey patch at all
monkeyPatchDomAPIs();
ConnectingSlot.set(elm, connectRootElement);
DisconnectingSlot.set(elm, disconnectRootElement);
}
};

let connectedCallback: LifecycleCallback | undefined;
let disconnectedCallback: LifecycleCallback | undefined;

if (lwcRuntimeFlags.ENABLE_NATIVE_CUSTOM_ELEMENT_LIFECYCLE) {
connectedCallback = (elm: HTMLElement) => {
const connectedCallback = (elm: HTMLElement) => {
if (useNativeCustomElementLifecycle) {
connectRootElement(elm);
};
disconnectedCallback = (elm: HTMLElement) => {
}
};
const disconnectedCallback = (elm: HTMLElement) => {
if (useNativeCustomElementLifecycle) {
disconnectRootElement(elm);
};
}
}
};

const element = createCustomElement(
tagName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,67 +8,58 @@ import { isUndefined } from '@lwc/shared';
import type { LifecycleCallback } from '@lwc/engine-core';

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

let elementBeingUpgradedByLWC = false;
let upgradeCallbackToUse: LifecycleCallback | undefined;
let connectedCallbackToUse: LifecycleCallback | undefined;
let disconnectedCallbackToUse: LifecycleCallback | undefined;

const instancesToConnectedCallbacks = new WeakMap<HTMLElement, LifecycleCallback>();
const instancesToDisconnectedCallbacks = 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 hasConnectedCallback = !isUndefined(connectedCallback);
const hasDisconnectedCallback = !isUndefined(disconnectedCallback);

const createUpgradableConstructor = () => {
// TODO [#2972]: this class should expose observedAttributes as necessary
class UpgradableConstructor extends HTMLElement {
constructor(upgradeCallback: LifecycleCallback) {
return class UpgradableConstructor extends HTMLElement {
constructor() {
super();
// If the element is not created using lwc.createElement(), e.g. `document.createElement('x-foo')`,
// then elementBeingUpgraded will be false
if (elementBeingUpgradedByLWC) {
upgradeCallback(this);
} else if (hasConnectedCallback || hasDisconnectedCallback) {
// If this element has connected or disconnected callbacks, then we need to keep track of
// instances that were created outside LWC (i.e. not created by `lwc.createElement()`).
// If the element has no connected or disconnected callbacks, then we don't need to track this.
elementsUpgradedOutsideLWC.add(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.
// Cache the callbacks in the weak maps
instancesToConnectedCallbacks.set(this, connectedCallbackToUse!);
instancesToDisconnectedCallbacks.set(this, disconnectedCallbackToUse!);
upgradeCallbackToUse!(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.
}
}

// Do not unnecessarily add a connectedCallback/disconnectedCallback, as it introduces perf overhead
// See: https://github.com/salesforce/lwc/pull/3162#issuecomment-1311851174
if (hasConnectedCallback) {
(UpgradableConstructor.prototype as any).connectedCallback = function () {
if (!elementsUpgradedOutsideLWC.has(this)) {
connectedCallback() {
const connectedCallback = instancesToConnectedCallbacks.get(this);
if (!isUndefined(connectedCallback)) {
connectedCallback(this);
}
};
}
}

if (hasDisconnectedCallback) {
(UpgradableConstructor.prototype as any).disconnectedCallback = function () {
if (!elementsUpgradedOutsideLWC.has(this)) {
disconnectedCallback() {
const disconnectedCallback = instancesToDisconnectedCallbacks.get(this);
if (!isUndefined(disconnectedCallback)) {
disconnectedCallback(this);
}
};
}

return UpgradableConstructor;
}
};
};

export const createCustomElementUsingUpgradableConstructor = (
tagName: string,
upgradeCallback: LifecycleCallback,
connectedCallback?: LifecycleCallback,
disconnectedCallback?: LifecycleCallback
connectedCallback: LifecycleCallback,
disconnectedCallback: LifecycleCallback
) => {
// use global custom elements registry
let UpgradableConstructor = cachedConstructors.get(tagName);
Expand All @@ -79,18 +70,21 @@ export const createCustomElementUsingUpgradableConstructor = (
`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);
}

elementBeingUpgradedByLWC = true;
upgradeCallbackToUse = upgradeCallback;
connectedCallbackToUse = connectedCallback;
disconnectedCallbackToUse = disconnectedCallback;
try {
return new UpgradableConstructor(upgradeCallback);
return new UpgradableConstructor();
} finally {
elementBeingUpgradedByLWC = false;
upgradeCallbackToUse = undefined;
connectedCallbackToUse = undefined;
disconnectedCallbackToUse = undefined;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import type { LifecycleCallback } from '@lwc/engine-core';
export let createCustomElement: (
tagName: string,
upgradeCallback: LifecycleCallback,
connectedCallback?: LifecycleCallback,
disconnectedCallback?: LifecycleCallback
connectedCallback: LifecycleCallback,
disconnectedCallback: LifecycleCallback
) => HTMLElement;

if (hasCustomElements) {
Expand Down

0 comments on commit 670593f

Please sign in to comment.