Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<template>
<x-child>
<!-- TODO [#5020]: Fix rendering of scoped slot content, so that content outside of the template renders correctly with engine-server -->
<span>Slotted content outside of template</span>
<template lwc:slot-data="data">
<span>Slotted content within template {data.id}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ export const expectedFailures = new Set([
'exports/component-as-default/index.js',
'known-boolean-attributes/default-def-html-attributes/static-on-component/index.js',
'render-dynamic-value/index.js',
'scoped-slots/advanced/index.js',
'scoped-slots/default-slot/index.js',
'scoped-slots/mixed-with-light-dom-slots-inside/index.js',
'scoped-slots/mixed-with-light-dom-slots-outside/index.js',
'slot-forwarding/slots/mixed/index.js',
'slot-forwarding/slots/dangling/index.js',
'wire/errors/throws-on-computed-key/index.js',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const bGenerateMarkup = esTemplate`
attrs,
shadowSlottedContent,
lightSlottedContent,
scopedSlottedContent,
parent,
scopeToken,
contextfulParent
Expand Down Expand Up @@ -67,6 +68,7 @@ const bGenerateMarkup = esTemplate`
yield* tmplFn(
shadowSlottedContent,
lightSlottedContent,
scopedSlottedContent,
${/*component class*/ 3},
instance
);
Expand Down
1 change: 1 addition & 0 deletions packages/@lwc/ssr-compiler/src/compile-template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const bExportTemplate = esTemplate`
export default async function* tmpl(
shadowSlottedContent,
lightSlottedContent,
scopedSlottedContent,
Cmp,
instance
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const bYieldFromChildGenerator = esTemplateWithYield`
/*
Slotted content is inserted here.
Note that the slotted content will be stored in variables named
`shadowSlottedContent`/`lightSlottedContentMap` which are used below
`shadowSlottedContent`/`lightSlottedContentMap / scopedSlottedContentMap` which are used below
when the child's generateMarkup function is invoked.
*/
is.statement
Expand All @@ -38,6 +38,7 @@ const bYieldFromChildGenerator = esTemplateWithYield`
childAttrs,
shadowSlottedContent,
lightSlottedContentMap,
scopedSlottedContentMap,
instance,
scopeToken,
contextfulParent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,21 @@ const bYieldFromDynamicComponentConstructorGenerator = esTemplateWithYield`
/*
Slotted content is inserted here.
Note that the slotted content will be stored in variables named
`shadowSlottedContent`/`lightSlottedContentMap` which are used below
`shadowSlottedContent`/`lightSlottedContentMap / scopedSlottedContentMap` which are used below
when the child's generateMarkup function is invoked.
*/
is.statement
}

const scopeToken = hasScopedStylesheets ? stylesheetScopeToken : undefined;

yield* Ctor[__SYMBOL__GENERATE_MARKUP](
null,
childProps,
childAttrs,
shadowSlottedContent,
lightSlottedContentMap,
scopedSlottedContentMap,
instance,
scopeToken,
contextfulParent
Expand All @@ -60,7 +61,6 @@ export const LwcComponent: Transformer<IrLwcComponent> = function LwcComponent(n
LightningElement: undefined,
SYMBOL__GENERATE_MARKUP: '__SYMBOL__GENERATE_MARKUP',
});

return bYieldFromDynamicComponentConstructorGenerator(
// The template compiler has validation to prevent lwcIs.value from being a literal
expressionIrToEs(lwcIs.value as IrExpression, cxt),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,18 @@ const bGenerateSlottedContent = esTemplateWithYield`
// Avoid creating the object unnecessarily
: null;

function addLightContent(name, fn) {
let contentList = lightSlottedContentMap[name];
// The containing slot treats scoped slotted content differently.
const scopedSlottedContentMap = ${/* hasScopedSlottedContent */ is.literal}
? Object.create(null)
// Avoid creating the object unnecessarily
: null;

function addSlottedContent(name, fn, contentMap) {
let contentList = contentMap[name];
if (contentList) {
contentList.push(fn);
} else {
lightSlottedContentMap[name] = [fn];
contentMap[name] = [fn];
}
}

Expand All @@ -69,13 +75,13 @@ const bGenerateSlottedContent = esTemplateWithYield`
// Note that this function name (`generateSlottedContent`) does not need to be scoped even though
// it may be repeated multiple times in the same scope, because it's a function _expression_ rather
// than a function _declaration_, so it isn't available to be referenced anywhere.
const bAddLightContent = esTemplate`
addLightContent(${/* slot name */ is.expression} ?? "", async function* generateSlottedContent(contextfulParent, ${
const bAddSlottedContent = esTemplate`
addSlottedContent(${/* slot name */ is.expression} ?? "", async function* generateSlottedContent(contextfulParent, ${
/* scoped slot data variable */ isNullableOf(is.identifier)
}) {
// FIXME: make validation work again
${/* slot content */ false}
});
}, ${/* content map */ is.identifier});
`<EsCallExpression>;

function getShadowSlottedContent(slottableChildren: IrChildNode[], cxt: TransformerContext) {
Expand Down Expand Up @@ -152,7 +158,16 @@ function getLightSlottedContent(rootNodes: IrChildNode[], cxt: TransformerContex
cxt.isSlotted = ancestorIndices.length > 1 || clone.type === 'Slot';
const slotContent = irToEs(clone, cxt);
cxt.isSlotted = originalIsSlotted;
results.push(b.expressionStatement(bAddLightContent(slotName, null, slotContent)));
results.push(
b.expressionStatement(
bAddSlottedContent(
slotName,
null,
slotContent,
b.identifier('lightSlottedContentMap')
)
)
);
};

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

// TODO [#4768]: what if the bound variable is `generateMarkup` or some framework-specific identifier?
const addLightContentExpr = b.expressionStatement(
bAddLightContent(slotName, boundVariable, irChildrenToEs(child.children, cxt))
bAddSlottedContent(
slotName,
boundVariable,
irChildrenToEs(child.children, cxt),
b.identifier('scopedSlottedContentMap')
)
);
cxt.popLocalVars();
return addLightContentExpr;
});

const hasShadowSlottedContent = b.literal(shadowSlotContent.length > 0);
const hasLightSlottedContent = b.literal(
lightSlotContent.length > 0 || scopedSlotContent.length > 0
);

const hasLightSlottedContent = b.literal(lightSlotContent.length > 0);
const hasScopedSlottedContent = b.literal(scopedSlotContent.length > 0);
cxt.isSlotted = isSlotted;

return bGenerateSlottedContent(
hasShadowSlottedContent,
shadowSlotContent,
hasLightSlottedContent,
hasScopedSlottedContent,
lightSlotContent,
scopedSlotContent
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,33 @@ const bConditionalSlot = esTemplateWithYield`
if (isLightDom) {
const isScopedSlot = ${/* isScopedSlot */ is.literal};
const isSlotted = ${/* isSlotted */ is.literal};
const slotName = ${/* slotName */ is.expression};
const lightGenerators = lightSlottedContent?.[slotName ?? ""];
const scopedGenerators = scopedSlottedContent?.[slotName ?? ""];
const mismatchedSlots = (isScopedSlot && lightGenerators) || (!isScopedSlot && scopedGenerators);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For mismatched slot case an error is logged in v1 / client (we don't throw). Error is logged non-production only. There was no precedence in v2 for this so do we need to do anything? Throwing would be too much, it needs to recover.

Copy link
Contributor

Choose a reason for hiding this comment

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

Logged errors are essentially meaningless, so I don't think we need to do anything here. It would be nice to have error logging somewhere in v2, but also not a big deal, since presumably folks can see the same logged error in their browser DevTools.

(As an aside, it should probably be a console.warn too! #3692)

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const mismatchedSlots = (isScopedSlot && lightGenerators) || (!isScopedSlot && scopedGenerators);
const mismatchedSlots = isScopedSlot ? lightGenerators : scopedGenerators;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, that is more elegant

const generators = isScopedSlot ? scopedGenerators : lightGenerators;

// start bookend HTML comment for light DOM slot vfragment
if (!isSlotted) {
yield '<!---->';

// scoped slot factory has its own vfragment hence its own bookend
if (isScopedSlot) {
// If there is slot data, scoped slot factory has its own vfragment hence its own bookend
if (isScopedSlot && generators) {
yield '<!---->';
}
}

const generators = lightSlottedContent?.[${/* slotName */ is.expression} ?? ""];
if (generators) {
for (const generator of generators) {
yield* generator(contextfulParent, ${/* scoped slot data */ isNullableOf(is.expression)});
for (let i = 0; i < generators.length; i++) {
yield* generators[i](contextfulParent, ${/* scoped slot data */ isNullableOf(is.expression)});
// Bookends after all but last scoped slot data
if (isScopedSlot && i < generators.length - 1) {
yield '<!---->';
yield '<!---->';
Comment on lines +49 to +50
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
yield '<!---->';
yield '<!---->';
yield '<!----><!---->';

}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is really bizarre behavior! I wonder what explains it.

Copy link
Contributor

@nolanlawson nolanlawson Jan 13, 2025

Choose a reason for hiding this comment

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

Oh wait I think I see – it's because of the double bookend? So the double bookend logic for the start/end is handled elsewhere?

It might be good to add a comment explaining this!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep exactly. Added a note to explain that!

}
} else {
// If there were mismatched slots, do not fallback to the default
} else if (!mismatchedSlots) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd love to understand why mismatched slots have any impact here.

Copy link
Contributor Author

@jhefferman-sfdc jhefferman-sfdc Jan 13, 2025

Choose a reason for hiding this comment

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

Added a comment here to explain: This is required for parity with engine-core which resets children to an empty array when there are children (mismatched or not). Because the child nodes are reset, the default slotted content is not rendered in the mismatched slot case. See here. I'm guessing this is a sort-of-bug but fixing it would mean a v1/client behavior change and I don't think it's worth it....

// If we're in this else block, then the generator _must_ have yielded
// something. It's impossible for a slottedContent["foo"] to exist
// without the generator yielding at least a text node / element.
Expand All @@ -53,8 +64,8 @@ const bConditionalSlot = esTemplateWithYield`
if (!isSlotted) {
yield '<!---->';

// scoped slot factory has its own vfragment hence its own bookend
if (isScopedSlot) {
// If there is slot data, scoped slot factory has its own vfragment hence its own bookend
if (isScopedSlot && generators) {
yield '<!---->';
}
}
Expand Down
8 changes: 8 additions & 0 deletions packages/@lwc/ssr-runtime/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export function renderAttrsNoYield(
export function* fallbackTmpl(
_shadowSlottedContent: unknown,
_lightSlottedContent: unknown,
_scopedSlottedContent: unknown,
Cmp: LightningElementConstructor,
_instance: unknown
) {
Expand All @@ -115,6 +116,7 @@ export function fallbackTmplNoYield(
emit: (segment: string) => void,
_shadowSlottedContent: unknown,
_lightSlottedContent: unknown,
_scopedSlottedContent: unknown,
Cmp: LightningElementConstructor,
_instance: unknown
) {
Expand All @@ -129,6 +131,7 @@ export type GenerateMarkupFn = (
attrs: Attributes | null,
shadowSlottedContent: AsyncGenerator<string> | null,
lightSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
scopedSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
// Not always null when invoked internally, but should always be
// null when invoked by ssr-runtime
parent: LightningElement | null,
Expand All @@ -143,6 +146,7 @@ export type GenerateMarkupFnAsyncNoGen = (
attrs: Attributes | null,
shadowSlottedContent: AsyncGenerator<string> | null,
lightSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
scopedSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
// Not always null when invoked internally, but should always be
// null when invoked by ssr-runtime
parent: LightningElement | null,
Expand All @@ -157,6 +161,7 @@ export type GenerateMarkupFnSyncNoGen = (
attrs: Attributes | null,
shadowSlottedContent: AsyncGenerator<string> | null,
lightSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
scopedSlottedContent: Record<number | string, AsyncGenerator<string>> | null,
// Not always null when invoked internally, but should always be
// null when invoked by ssr-runtime
parent: LightningElement | null,
Expand Down Expand Up @@ -199,6 +204,7 @@ export async function serverSideRenderComponent(
null,
null,
null,
null,
null
)) {
markup += segment;
Expand All @@ -213,6 +219,7 @@ export async function serverSideRenderComponent(
null,
null,
null,
null,
null
);
} else if (mode === 'sync') {
Expand All @@ -225,6 +232,7 @@ export async function serverSideRenderComponent(
null,
null,
null,
null,
null
);
} else {
Expand Down