Skip to content

Commit

Permalink
fix(ssr): non-top-level light slots with if:true (#4967)
Browse files Browse the repository at this point in the history
Co-authored-by: Will Harney <[email protected]>
  • Loading branch information
nolanlawson and wjhsf authored Nov 27, 2024
1 parent 7ceab63 commit 15297aa
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,9 @@ export const expectedFailures = new Set([
'scoped-slots/mixed-with-light-dom-slots-inside/index.js',
'scoped-slots/mixed-with-light-dom-slots-outside/index.js',
'slot-forwarding/scoped-slots/index.js',
'slot-not-at-top-level/advanced/ifTrue/light/index.js',
'slot-not-at-top-level/advanced/ifTrue/shadow/index.js',
'slot-not-at-top-level/advanced/lwcIf/light/index.js',
'slot-not-at-top-level/advanced/lwcIf/shadow/index.js',
'slot-not-at-top-level/ifTrue/light/index.js',
'slot-not-at-top-level/ifTrue/shadow/index.js',
'slot-not-at-top-level/lwcIf/light/index.js',
'slot-not-at-top-level/lwcIf/shadow/index.js',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,22 @@ import { irChildrenToEs, irToEs } from '../../ir-to-es';
import { isNullableOf } from '../../../estree/validators';
import type { CallExpression as EsCallExpression, Expression as EsExpression } from 'estree';

import type { Statement as EsStatement } from 'estree';
import type {
Statement as EsStatement,
ExpressionStatement as EsExpressionStatement,
} from 'estree';
import type {
ChildNode as IrChildNode,
Component as IrComponent,
Element as IrElement,
ElseBlock as IrElseBlock,
ElseifBlock as IrElseifBlock,
ExternalComponent as IrExternalComponent,
If as IrIf,
IfBlock as IrIfBlock,
LwcComponent as IrLwcComponent,
ScopedSlotFragment,
ScopedSlotFragment as IrScopedSlotFragment,
Text as IrText,
} from '@lwc/template-compiler';
import type { TransformerContext } from '../../types';

Expand Down Expand Up @@ -62,6 +73,94 @@ const bAddLightContent = esTemplate`
});
`<EsCallExpression>;

// Light DOM slots are a bit complex because of needing to handle slots _not_ at the top level
// At the non-top level, it matters what the ancestors are. These are relevant to slots:
// - If (`if:true`, `if:false`)
// - IfBlock/ElseBlock/ElseifBlock (`lwc:if`, `lwc:elseif`, `lwc:else`)
// Whereas anything else breaks the relationship between the slotted content and the containing
// Component (e.g. another Component/ExternalComponent) or is disallowed (e.g. ForEach/ForOf).
// Then there are the leaf nodes, which _may_ have a `slot` attribute on them:
// - Element/Text/Component/ExternalComponent (e.g. `<div>`, `<x-foo>`)
// Once you reach a leaf, you know what content should be rendered for a given slot name. But you
// also need to consider all of its ancestors, which may cause the slot content to be conditionally
// rendered (e.g. IfBlock/ElseBlock).
// For example:
// <x-foo>
// <template lwc:if={darkTheme}>
// <div slot="footer"></div>
// </template>
// <template lwc:else>
// yolo
// </template>
// </x-foo>
// In this example, we render the `<div>` into the `footer` slot, if `darkTheme` is true.
// Otherwise, we will render the text node `yolo` into the default slot.
// The goal here is to traverse through the tree and identify all unique `slot` attribute names
// and group those into AST trees on a per-`slot` name basis, only for leafs/ancestors that are
// relevant to slots (as mentioned above).
function getLightSlottedContent(rootNodes: IrChildNode[], cxt: TransformerContext) {
type SlottableAncestorIrType = IrIf | IrIfBlock | IrElseifBlock | IrElseBlock;
type SlottableLeafIrType = IrElement | IrText | IrComponent | IrExternalComponent;

const results: EsExpressionStatement[] = [];

// For the given slot name, get the EsExpressions we should use to render it
// The ancestorIndices is an array of integers referring to the chain of ancestors
// and their positions in the child arrays of their own parents
const addLightDomSlotContent = (slotName: EsExpression, ancestorIndices: number[]) => {
const clone = produce(rootNodes[ancestorIndices[0]], (draft) => {
// Create a clone of the AST with only the ancestors and no other siblings
let current = draft;
for (let i = 1; i < ancestorIndices.length; i++) {
const nextIndex = ancestorIndices[i];

// If i >= 1 then the current must necessarily be a SlottableAncestorIrType
const next = (current as SlottableAncestorIrType).children[nextIndex];
(current as SlottableAncestorIrType).children = [next];
current = next;
}
// The leaf must necessarily be a SlottableLeafIrType
const leaf = current as SlottableLeafIrType;
// Light DOM slots do not actually render the `slot` attribute.
if (leaf.type !== 'Text') {
leaf.attributes = leaf.attributes.filter((attr) => attr.name !== 'slot');
}
});
const slotContent = irToEs(clone, cxt);
results.push(b.expressionStatement(bAddLightContent(slotName, null, slotContent)));
};

const traverse = (nodes: IrChildNode[], ancestorIndices: number[]) => {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
switch (node.type) {
// SlottableAncestorIrType
case 'If':
case 'IfBlock':
case 'ElseifBlock':
case 'ElseBlock': {
traverse(node.children, [...ancestorIndices, i]);
break;
}
// SlottableLeafIrType
case 'Element':
case 'Text':
case 'Component':
case 'ExternalComponent': {
// '' is the default slot name. Text nodes are always slotted into the default slot
const slotName =
node.type === 'Text' ? b.literal('') : bAttributeValue(node, 'slot');
addLightDomSlotContent(slotName, [...ancestorIndices, i]);
break;
}
}
}
};

traverse(rootNodes, []);
return results;
}

export function getSlottedContent(
node: IrLwcComponent | IrComponent,
cxt: TransformerContext
Expand All @@ -70,23 +169,11 @@ export function getSlottedContent(
const slottableChildren = node.children.filter((child) => child.type !== 'ScopedSlotFragment');
const scopedSlottableChildren = node.children.filter(
(child) => child.type === 'ScopedSlotFragment'
) as ScopedSlotFragment[];
) as IrScopedSlotFragment[];

const shadowSlotContent = optimizeAdjacentYieldStmts(irChildrenToEs(slottableChildren, cxt));

const lightSlotContent = slottableChildren.map((child) => {
if ('attributes' in child) {
const slotName = bAttributeValue(child, 'slot');
// Light DOM slots do not actually render the `slot` attribute.
const clone = produce(child, (draft) => {
draft.attributes = draft.attributes.filter((attr) => attr.name !== 'slot');
});
const slotContent = irToEs(clone, cxt);
return b.expressionStatement(bAddLightContent(slotName, null, slotContent));
} else {
return b.expressionStatement(bAddLightContent(b.literal(''), null, irToEs(child, cxt)));
}
});
const lightSlotContent = getLightSlottedContent(slottableChildren, cxt);

const scopedSlotContent = scopedSlottableChildren.map((child) => {
const boundVariableName = child.slotData.value.name;
Expand Down

0 comments on commit 15297aa

Please sign in to comment.