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: use fragments for slots in light DOM (BREAKING CHANGE) #3649

Merged
merged 9 commits into from
Oct 20, 2023
5 changes: 5 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,11 @@ jobs:
- run_karma:
disable_synthetic: true
api_version: 58
- run_karma:
api_version: 59
- run_karma:
disable_synthetic: true
api_version: 59
- run_karma:
disable_synthetic: true
disable_aria_reflection_polyfill: true
Expand Down
37 changes: 22 additions & 15 deletions packages/@lwc/engine-core/src/framework/api.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 {
APIFeature,
ArrayPush,
assert,
create as ObjectCreate,
freeze as ObjectFreeze,
forEach,
freeze as ObjectFreeze,
isAPIFeatureEnabled,
isArray,
isFalse,
isFunction,
Expand All @@ -29,24 +31,24 @@ import { invokeEventListener } from './invoker';
import { getVMBeingRendered, setVMBeingRendered } from './template';
import { EmptyArray, setRefVNode } from './utils';
import { isComponentConstructor } from './def';
import { ShadowMode, SlotSet, VM, RenderMode } from './vm';
import { RenderMode, ShadowMode, SlotSet, VM } from './vm';
import { LightningElementConstructor } from './base-lightning-element';
import { markAsDynamicChildren } from './rendering';
import {
VNode,
VNodes,
VElement,
VText,
VCustomElement,
isVScopedSlotFragment,
Key,
VComment,
VCustomElement,
VElement,
VElementData,
VNodeType,
VStatic,
Key,
VFragment,
isVScopedSlotFragment,
VNode,
VNodes,
VNodeType,
VScopedSlotFragment,
VStatic,
VStaticElementData,
VText,
} from './vnodes';
import { getComponentRegisteredName } from './component';

Expand Down Expand Up @@ -192,7 +194,7 @@ function s(
data: VElementData,
children: VNodes,
slotset: SlotSet | undefined
): VElement | VNodes {
): VElement | VNodes | VFragment {
if (process.env.NODE_ENV !== 'production') {
assert.isTrue(isString(slotName), `s() 1st argument slotName must be a string.`);
assert.isTrue(isObject(data), `s() 2nd argument data must be an object.`);
Expand Down Expand Up @@ -252,11 +254,16 @@ function s(
children = newChildren;
}
const vmBeingRendered = getVMBeingRendered()!;
const { renderMode, shadowMode } = vmBeingRendered;
const { renderMode, shadowMode, apiVersion } = vmBeingRendered;

if (renderMode === RenderMode.Light) {
sc(children);
return children;
// light DOM slots - backwards-compatible behavior uses flattening, new behavior uses fragments
if (isAPIFeatureEnabled(APIFeature.USE_FRAGMENTS_FOR_LIGHT_DOM_SLOTS, apiVersion)) {
return fr(data.key, children, 0);
} else {
sc(children);
return children;
}
}
if (shadowMode === ShadowMode.Synthetic) {
// TODO [#1276]: compiler should give us some sort of indicator when a vnodes collection is dynamic
Expand Down
14 changes: 13 additions & 1 deletion packages/@lwc/engine-core/src/framework/vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import {
APIVersion,
ArrayPush,
ArraySlice,
ArrayUnshift,
Expand All @@ -25,7 +26,12 @@ import { addErrorComponentStack } from '../shared/error';
import { logError, logWarnOnce } from '../shared/logger';

import { HostNode, HostElement, RendererAPI } from './renderer';
import { renderComponent, markComponentAsDirty, getTemplateReactiveObserver } from './component';
import {
renderComponent,
markComponentAsDirty,
getTemplateReactiveObserver,
getComponentAPIVersion,
} from './component';
import { addCallbackToNextTick, EmptyArray, EmptyObject, flattenStylesheets } from './utils';
import { invokeServiceHook, Services } from './services';
import { invokeComponentCallback, invokeComponentConstructor } from './invoker';
Expand Down Expand Up @@ -185,6 +191,10 @@ export interface VM<N = HostNode, E = HostElement> {
/**
* Any stylesheets associated with the component */
stylesheets: TemplateStylesheetFactories | null;
/**
* API version associated with this VM
*/
apiVersion: APIVersion;
}

type VMAssociable = HostNode | LightningElement;
Expand Down Expand Up @@ -289,6 +299,7 @@ export function createVM<HostNode, HostElement>(
): VM {
const { mode, owner, tagName, hydrated } = options;
const def = getComponentInternalDef(ctor);
const apiVersion = getComponentAPIVersion(ctor);

const vm: VM = {
elm,
Expand Down Expand Up @@ -337,6 +348,7 @@ export function createVM<HostNode, HostElement>(
getHook,

renderer,
apiVersion,
};

if (process.env.NODE_ENV !== 'production') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
<template shadowroot="open">
<x-basic-child>
<div>
<span>
99 - ssr
</span>
</div>
</x-basic-child>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@
<x-child-with-for-each>
<ul>
<li>
<span>
39 - Audio
</span>
</li>
<li>
<span>
40 - Video
</span>
</li>
</ul>
</x-child-with-for-each>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
<template shadowroot="open">
<x-child>
<div>
<span>
99 - ssr
</span>
</div>
<p>
Default slot - default content
Default slot - default content
</p>
</x-child>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('Slotting', () => {
it('should render properly', () => {
const nodes = createTestElement('x-default-slot', BasicSlot);

expect(Array.from(nodes['x-container'].childNodes)).toEqual([
expect(Array.from(nodes['x-container'].children)).toEqual([
nodes['upper-text'],
nodes['default-text'],
nodes['lower-text'],
Expand All @@ -29,7 +29,7 @@ describe('Slotting', () => {

it('should render dynamic children', async () => {
const nodes = createTestElement('x-dynamic-children', DynamicChildren);
expect(Array.from(nodes['x-light-container'].childNodes)).toEqual([
expect(Array.from(nodes['x-light-container'].children)).toEqual([
nodes['container-upper-slot-default'],
nodes['1'],
nodes['2'],
Expand All @@ -42,7 +42,7 @@ describe('Slotting', () => {
nodes.button.click();
await Promise.resolve();

expect(Array.from(nodes['x-light-container'].childNodes)).toEqual([
expect(Array.from(nodes['x-light-container'].children)).toEqual([
nodes['container-upper-slot-default'],
nodes['5'],
nodes['4'],
Expand Down Expand Up @@ -80,10 +80,10 @@ describe('Slotting', () => {
it('removes slots properly', async () => {
const nodes = createTestElement('x-conditional-slot', ConditionalSlot);
const elm = nodes['x-conditional-slot'];
expect(Array.from(elm.childNodes)).toEqual([nodes['default-slotted-text'], nodes.button]);
expect(Array.from(elm.children)).toEqual([nodes['default-slotted-text'], nodes.button]);
nodes.button.click();
await Promise.resolve();
expect(Array.from(elm.childNodes)).toEqual([nodes.button]);
expect(Array.from(elm.children)).toEqual([nodes.button]);
});

it('removes slotted content properly', async () => {
Expand Down Expand Up @@ -116,4 +116,19 @@ describe('Slotting', () => {
'<x-forwarded-slot><x-light-container><p data-id="container-upper-slot-default">Upper slot default</p>Default slot not yet forwarded<p data-id="container-lower-slot-default">Lower slot default</p></x-light-container></x-forwarded-slot>'
);
});

it('should only generate empty text nodes for APIVersion >=60', async () => {
const elm = createElement('x-default-slot', { is: BasicSlot });
document.body.appendChild(elm);
await Promise.resolve();
const container = elm.querySelector('x-light-container');
const emptyTextNodes = [...container.childNodes].filter(
(_) => _.nodeType === Node.TEXT_NODE && _.data === ''
);
if (process.env.API_VERSION <= 59) {
expect(emptyTextNodes.length).toBe(0); // old implementation does not use fragments, just flattening
} else {
expect(emptyTextNodes.length).toBe(6); // 3 slots, so 3*2=6 empty text nodes
}
});
});
15 changes: 13 additions & 2 deletions packages/@lwc/shared/src/api-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@ import { isNumber } from './language';
export const enum APIVersion {
V58_244_SUMMER_23 = 58,
V59_246_WINTER_24 = 59,
V60_248_SPRING_24 = 60,
}

// These must be updated when the enum is updated.
// It's a bit annoying to do have to do this manually, but this makes the file tree-shakeable,
// passing the `verify-treeshakeable.js` test.

export const LOWEST_API_VERSION = APIVersion.V58_244_SUMMER_23;
export const HIGHEST_API_VERSION = APIVersion.V59_246_WINTER_24;
const allVersions = [APIVersion.V58_244_SUMMER_23, APIVersion.V59_246_WINTER_24];
export const HIGHEST_API_VERSION = APIVersion.V60_248_SPRING_24;
const allVersions = [
APIVersion.V58_244_SUMMER_23,
APIVersion.V59_246_WINTER_24,
APIVersion.V60_248_SPRING_24,
];
const allVersionsSet = /*@__PURE__@*/ new Set(allVersions);

export function getAPIVersionFromNumber(version: number | undefined): APIVersion {
Expand Down Expand Up @@ -54,6 +59,10 @@ export const enum APIFeature {
* (for backwards compatibility).
*/
TREAT_ALL_PARSE5_ERRORS_AS_ERRORS,
/**
* If enabled, use fragments for slots in light DOM.
*/
USE_FRAGMENTS_FOR_LIGHT_DOM_SLOTS,
}

export function isAPIFeatureEnabled(
Expand All @@ -64,5 +73,7 @@ export function isAPIFeatureEnabled(
case APIFeature.LOWERCASE_SCOPE_TOKENS:
case APIFeature.TREAT_ALL_PARSE5_ERRORS_AS_ERRORS:
return apiVersion >= APIVersion.V59_246_WINTER_24;
case APIFeature.USE_FRAGMENTS_FOR_LIGHT_DOM_SLOTS:
return apiVersion >= APIVersion.V60_248_SPRING_24;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,13 @@ const stc2 = {
key: 4,
};
function tmpl($api, $cmp, $slotset, $ctx) {
const {
st: api_static_fragment,
t: api_text,
s: api_slot,
f: api_flatten,
} = $api;
return api_flatten([
const { st: api_static_fragment, t: api_text, s: api_slot } = $api;
return [
api_static_fragment($fragment1(), 1),
api_slot("", stc0, [api_text("Default")], $slotset),
api_slot("named", stc1, [api_text("Named")], $slotset),
api_slot("forwarded", stc2, [api_text("Forwarded")], $slotset),
]);
];
/*LWC compiler vX.X.X*/
}
export default registerTemplate(tmpl);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template lwc:render-mode="light">
<slot lwc:slot-bind={item}></slot>
</template>
Loading