Skip to content

Commit ac292ce

Browse files
fix: do not render mismatched scoped slotted data
1 parent f080072 commit ac292ce

File tree

9 files changed

+66
-29
lines changed

9 files changed

+66
-29
lines changed

packages/@lwc/engine-server/src/__tests__/fixtures/scoped-slots/default-slot/modules/x/parent/parent.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<template>
22
<x-child>
3-
<!-- TODO [#5020]: Fix rendering of scoped slot content, so that content outside of the template renders correctly with engine-server -->
43
<span>Slotted content outside of template</span>
54
<template lwc:slot-data="data">
65
<span>Slotted content within template {data.id}</span>

packages/@lwc/ssr-compiler/src/__tests__/utils/expected-failures.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@ export const expectedFailures = new Set([
1313
'exports/component-as-default/index.js',
1414
'known-boolean-attributes/default-def-html-attributes/static-on-component/index.js',
1515
'render-dynamic-value/index.js',
16-
'scoped-slots/advanced/index.js',
17-
'scoped-slots/default-slot/index.js',
18-
'scoped-slots/mixed-with-light-dom-slots-inside/index.js',
19-
'scoped-slots/mixed-with-light-dom-slots-outside/index.js',
2016
'slot-forwarding/slots/mixed/index.js',
2117
'slot-forwarding/slots/dangling/index.js',
2218
'wire/errors/throws-on-computed-key/index.js',

packages/@lwc/ssr-compiler/src/compile-js/generate-markup.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const bGenerateMarkup = esTemplate`
2525
attrs,
2626
shadowSlottedContent,
2727
lightSlottedContent,
28+
scopedSlottedContent,
2829
parent,
2930
scopeToken,
3031
contextfulParent
@@ -67,6 +68,7 @@ const bGenerateMarkup = esTemplate`
6768
yield* tmplFn(
6869
shadowSlottedContent,
6970
lightSlottedContent,
71+
scopedSlottedContent,
7072
${/*component class*/ 3},
7173
instance
7274
);

packages/@lwc/ssr-compiler/src/compile-template/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const bExportTemplate = esTemplate`
2323
export default async function* tmpl(
2424
shadowSlottedContent,
2525
lightSlottedContent,
26+
scopedSlottedContent,
2627
Cmp,
2728
instance
2829
) {

packages/@lwc/ssr-compiler/src/compile-template/transformers/component/component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const bYieldFromChildGenerator = esTemplateWithYield`
2323
/*
2424
Slotted content is inserted here.
2525
Note that the slotted content will be stored in variables named
26-
`shadowSlottedContent`/`lightSlottedContentMap` which are used below
26+
`shadowSlottedContent`/`lightSlottedContentMap / scopedSlottedContentMap` which are used below
2727
when the child's generateMarkup function is invoked.
2828
*/
2929
is.statement
@@ -38,6 +38,7 @@ const bYieldFromChildGenerator = esTemplateWithYield`
3838
childAttrs,
3939
shadowSlottedContent,
4040
lightSlottedContentMap,
41+
scopedSlottedContentMap,
4142
instance,
4243
scopeToken,
4344
contextfulParent

packages/@lwc/ssr-compiler/src/compile-template/transformers/component/lwc-component.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,21 @@ const bYieldFromDynamicComponentConstructorGenerator = esTemplateWithYield`
2929
/*
3030
Slotted content is inserted here.
3131
Note that the slotted content will be stored in variables named
32-
`shadowSlottedContent`/`lightSlottedContentMap` which are used below
32+
`shadowSlottedContent`/`lightSlottedContentMap / scopedSlottedContentMap` which are used below
3333
when the child's generateMarkup function is invoked.
3434
*/
3535
is.statement
3636
}
3737
3838
const scopeToken = hasScopedStylesheets ? stylesheetScopeToken : undefined;
39-
39+
4040
yield* Ctor[__SYMBOL__GENERATE_MARKUP](
4141
null,
4242
childProps,
4343
childAttrs,
4444
shadowSlottedContent,
4545
lightSlottedContentMap,
46+
scopedSlottedContentMap,
4647
instance,
4748
scopeToken,
4849
contextfulParent
@@ -60,7 +61,6 @@ export const LwcComponent: Transformer<IrLwcComponent> = function LwcComponent(n
6061
LightningElement: undefined,
6162
SYMBOL__GENERATE_MARKUP: '__SYMBOL__GENERATE_MARKUP',
6263
});
63-
6464
return bYieldFromDynamicComponentConstructorGenerator(
6565
// The template compiler has validation to prevent lwcIs.value from being a literal
6666
expressionIrToEs(lwcIs.value as IrExpression, cxt),

packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,18 @@ const bGenerateSlottedContent = esTemplateWithYield`
5353
// Avoid creating the object unnecessarily
5454
: null;
5555
56-
function addLightContent(name, fn) {
57-
let contentList = lightSlottedContentMap[name];
56+
// The containing slot treats scoped slotted content differently.
57+
const scopedSlottedContentMap = ${/* hasScopedSlottedContent */ is.literal}
58+
? Object.create(null)
59+
// Avoid creating the object unnecessarily
60+
: null;
61+
62+
function addSlottedContent(name, fn, contentMap) {
63+
let contentList = contentMap[name];
5864
if (contentList) {
5965
contentList.push(fn);
6066
} else {
61-
lightSlottedContentMap[name] = [fn];
67+
contentMap[name] = [fn];
6268
}
6369
}
6470
@@ -69,13 +75,13 @@ const bGenerateSlottedContent = esTemplateWithYield`
6975
// Note that this function name (`generateSlottedContent`) does not need to be scoped even though
7076
// it may be repeated multiple times in the same scope, because it's a function _expression_ rather
7177
// than a function _declaration_, so it isn't available to be referenced anywhere.
72-
const bAddLightContent = esTemplate`
73-
addLightContent(${/* slot name */ is.expression} ?? "", async function* generateSlottedContent(contextfulParent, ${
78+
const bAddSlottedContent = esTemplate`
79+
addSlottedContent(${/* slot name */ is.expression} ?? "", async function* generateSlottedContent(contextfulParent, ${
7480
/* scoped slot data variable */ isNullableOf(is.identifier)
7581
}) {
7682
// FIXME: make validation work again
7783
${/* slot content */ false}
78-
});
84+
}, ${/* content map */ is.identifier});
7985
`<EsCallExpression>;
8086

8187
function getShadowSlottedContent(slottableChildren: IrChildNode[], cxt: TransformerContext) {
@@ -152,7 +158,16 @@ function getLightSlottedContent(rootNodes: IrChildNode[], cxt: TransformerContex
152158
cxt.isSlotted = ancestorIndices.length > 1 || clone.type === 'Slot';
153159
const slotContent = irToEs(clone, cxt);
154160
cxt.isSlotted = originalIsSlotted;
155-
results.push(b.expressionStatement(bAddLightContent(slotName, null, slotContent)));
161+
results.push(
162+
b.expressionStatement(
163+
bAddSlottedContent(
164+
slotName,
165+
null,
166+
slotContent,
167+
b.identifier('lightSlottedContentMap')
168+
)
169+
)
170+
);
156171
};
157172

158173
const traverse = (nodes: IrChildNode[], ancestorIndices: number[]) => {
@@ -229,23 +244,27 @@ export function getSlottedContent(
229244

230245
// TODO [#4768]: what if the bound variable is `generateMarkup` or some framework-specific identifier?
231246
const addLightContentExpr = b.expressionStatement(
232-
bAddLightContent(slotName, boundVariable, irChildrenToEs(child.children, cxt))
247+
bAddSlottedContent(
248+
slotName,
249+
boundVariable,
250+
irChildrenToEs(child.children, cxt),
251+
b.identifier('scopedSlottedContentMap')
252+
)
233253
);
234254
cxt.popLocalVars();
235255
return addLightContentExpr;
236256
});
237257

238258
const hasShadowSlottedContent = b.literal(shadowSlotContent.length > 0);
239-
const hasLightSlottedContent = b.literal(
240-
lightSlotContent.length > 0 || scopedSlotContent.length > 0
241-
);
242-
259+
const hasLightSlottedContent = b.literal(lightSlotContent.length > 0);
260+
const hasScopedSlottedContent = b.literal(scopedSlotContent.length > 0);
243261
cxt.isSlotted = isSlotted;
244262

245263
return bGenerateSlottedContent(
246264
hasShadowSlottedContent,
247265
shadowSlotContent,
248266
hasLightSlottedContent,
267+
hasScopedSlottedContent,
249268
lightSlotContent,
250269
scopedSlotContent
251270
);

packages/@lwc/ssr-compiler/src/compile-template/transformers/slot.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,33 @@ const bConditionalSlot = esTemplateWithYield`
2525
if (isLightDom) {
2626
const isScopedSlot = ${/* isScopedSlot */ is.literal};
2727
const isSlotted = ${/* isSlotted */ is.literal};
28+
const slotName = ${/* slotName */ is.expression};
29+
const lightGenerators = lightSlottedContent?.[slotName ?? ""];
30+
const scopedGenerators = scopedSlottedContent?.[slotName ?? ""];
31+
const mismatchedSlots = (isScopedSlot && lightGenerators) || (!isScopedSlot && scopedGenerators);
32+
const generators = isScopedSlot ? scopedGenerators : lightGenerators;
33+
2834
// start bookend HTML comment for light DOM slot vfragment
2935
if (!isSlotted) {
3036
yield '<!---->';
3137
32-
// scoped slot factory has its own vfragment hence its own bookend
33-
if (isScopedSlot) {
38+
// If there is slot data, scoped slot factory has its own vfragment hence its own bookend
39+
if (isScopedSlot && generators) {
3440
yield '<!---->';
3541
}
3642
}
3743
38-
const generators = lightSlottedContent?.[${/* slotName */ is.expression} ?? ""];
3944
if (generators) {
40-
for (const generator of generators) {
41-
yield* generator(contextfulParent, ${/* scoped slot data */ isNullableOf(is.expression)});
45+
for (let i = 0; i < generators.length; i++) {
46+
yield* generators[i](contextfulParent, ${/* scoped slot data */ isNullableOf(is.expression)});
47+
// Bookends after all but last scoped slot data
48+
if (isScopedSlot && i < generators.length - 1) {
49+
yield '<!---->';
50+
yield '<!---->';
51+
}
4252
}
43-
} else {
53+
// If there were mismatched slots, do not fallback to the default
54+
} else if (!mismatchedSlots) {
4455
// If we're in this else block, then the generator _must_ have yielded
4556
// something. It's impossible for a slottedContent["foo"] to exist
4657
// without the generator yielding at least a text node / element.
@@ -53,8 +64,8 @@ const bConditionalSlot = esTemplateWithYield`
5364
if (!isSlotted) {
5465
yield '<!---->';
5566
56-
// scoped slot factory has its own vfragment hence its own bookend
57-
if (isScopedSlot) {
67+
// If there is slot data, scoped slot factory has its own vfragment hence its own bookend
68+
if (isScopedSlot && generators) {
5869
yield '<!---->';
5970
}
6071
}

packages/@lwc/ssr-runtime/src/render.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export function renderAttrsNoYield(
103103
export function* fallbackTmpl(
104104
_shadowSlottedContent: unknown,
105105
_lightSlottedContent: unknown,
106+
_scopedSlottedContent: unknown,
106107
Cmp: LightningElementConstructor,
107108
_instance: unknown
108109
) {
@@ -115,6 +116,7 @@ export function fallbackTmplNoYield(
115116
emit: (segment: string) => void,
116117
_shadowSlottedContent: unknown,
117118
_lightSlottedContent: unknown,
119+
_scopedSlottedContent: unknown,
118120
Cmp: LightningElementConstructor,
119121
_instance: unknown
120122
) {
@@ -129,6 +131,7 @@ export type GenerateMarkupFn = (
129131
attrs: Attributes | null,
130132
shadowSlottedContent: AsyncGenerator<string> | null,
131133
lightSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
134+
scopedSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
132135
// Not always null when invoked internally, but should always be
133136
// null when invoked by ssr-runtime
134137
parent: LightningElement | null,
@@ -143,6 +146,7 @@ export type GenerateMarkupFnAsyncNoGen = (
143146
attrs: Attributes | null,
144147
shadowSlottedContent: AsyncGenerator<string> | null,
145148
lightSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
149+
scopedSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
146150
// Not always null when invoked internally, but should always be
147151
// null when invoked by ssr-runtime
148152
parent: LightningElement | null,
@@ -157,6 +161,7 @@ export type GenerateMarkupFnSyncNoGen = (
157161
attrs: Attributes | null,
158162
shadowSlottedContent: AsyncGenerator<string> | null,
159163
lightSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
164+
scopedSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
160165
// Not always null when invoked internally, but should always be
161166
// null when invoked by ssr-runtime
162167
parent: LightningElement | null,
@@ -199,6 +204,7 @@ export async function serverSideRenderComponent(
199204
null,
200205
null,
201206
null,
207+
null,
202208
null
203209
)) {
204210
markup += segment;
@@ -213,6 +219,7 @@ export async function serverSideRenderComponent(
213219
null,
214220
null,
215221
null,
222+
null,
216223
null
217224
);
218225
} else if (mode === 'sync') {
@@ -225,6 +232,7 @@ export async function serverSideRenderComponent(
225232
null,
226233
null,
227234
null,
235+
null,
228236
null
229237
);
230238
} else {

0 commit comments

Comments
 (0)