diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/error.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/expected.html new file mode 100644 index 0000000000..63ea1759fe --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/expected.html @@ -0,0 +1,16 @@ + + + + + + Component content + + + + + Component content + + + + + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/index.js b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/index.js new file mode 100644 index 0000000000..b202a15a0a --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/index.js @@ -0,0 +1,4 @@ +export const tagName = 'x-dangling-container'; +export { default } from 'x/container'; +export * from 'x/container'; +export const features = []; diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/component/component.html b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/component/component.html new file mode 100644 index 0000000000..17ad53e7e1 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/component/component.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/component/component.js b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/component/component.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/component/component.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/container/container.html b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/container/container.html new file mode 100644 index 0000000000..685193646f --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/container/container.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/container/container.js b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/container/container.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/container/container.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/leaf/leaf.html b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/leaf/leaf.html new file mode 100644 index 0000000000..dc9ac68111 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/leaf/leaf.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/leaf/leaf.js b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/leaf/leaf.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/leaf/leaf.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/slot/slot.html b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/slot/slot.html new file mode 100644 index 0000000000..963a3fceca --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/slot/slot.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/slot/slot.js b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/slot/slot.js new file mode 100644 index 0000000000..0679d2bc10 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling-component/modules/x/slot/slot.js @@ -0,0 +1,5 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/expected.html index aa23e8774b..adfb2329a5 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/expected.html +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/expected.html @@ -2,14 +2,14 @@ -

- bottom content +

+ top content

-

- top content -

+

+ bottom content +

diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/modules/x/container/container.html b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/modules/x/container/container.html index d6153f7e58..616c5495b8 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/modules/x/container/container.html +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/modules/x/container/container.html @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/modules/x/leaf/leaf.html b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/modules/x/leaf/leaf.html index 039365a7a7..dc9ac68111 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/modules/x/leaf/leaf.html +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/modules/x/leaf/leaf.html @@ -1,4 +1,4 @@ \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/modules/x/slot/slot.html b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/modules/x/slot/slot.html index e641874967..963a3fceca 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/modules/x/slot/slot.html +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/modules/x/slot/slot.html @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/packages/@lwc/ssr-compiler/src/__tests__/utils/expected-failures.ts b/packages/@lwc/ssr-compiler/src/__tests__/utils/expected-failures.ts index b2f8518054..4d02bf9f22 100644 --- a/packages/@lwc/ssr-compiler/src/__tests__/utils/expected-failures.ts +++ b/packages/@lwc/ssr-compiler/src/__tests__/utils/expected-failures.ts @@ -13,8 +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', - 'slot-forwarding/slots/mixed/index.js', - 'slot-forwarding/slots/dangling/index.js', 'wire/errors/throws-on-computed-key/index.js', 'wire/errors/throws-when-colliding-prop-then-method/index.js', ]); diff --git a/packages/@lwc/ssr-compiler/src/compile-template/index.ts b/packages/@lwc/ssr-compiler/src/compile-template/index.ts index 037d51299f..041ef68f94 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/index.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/index.ts @@ -32,6 +32,9 @@ const bExportTemplate = esTemplate` let textContentBuffer = ''; let didBufferTextContent = false; + // This will get overridden but requires initialization. + const slotAttributeValue = null; + // Establishes a contextual relationship between two components for ContextProviders. // This variable will typically get overridden (shadowed) within slotted content. const contextfulParent = instance; diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/component.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/component.ts index 305878fdd4..c02ffe8bf8 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/component.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/component.ts @@ -19,6 +19,13 @@ const bYieldFromChildGenerator = esTemplateWithYield` { const childProps = ${/* child props */ is.objectExpression}; const childAttrs = ${/* child attrs */ is.objectExpression}; + /* + If 'slotAttributeValue' is set, it references a slot that does not exist, and the 'slot' attribute should be set in the DOM. This behavior aligns with engine-server and engine-dom. + See: engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/ for example case. + */ + if (slotAttributeValue) { + childAttrs.slot = slotAttributeValue; + } ${ /* Slotted content is inserted here. diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/lwc-component.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/lwc-component.ts index b7076424c9..b6b7a518be 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/lwc-component.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/lwc-component.ts @@ -25,6 +25,13 @@ const bYieldFromDynamicComponentConstructorGenerator = esTemplateWithYield` } const childProps = ${/* child props */ is.objectExpression}; const childAttrs = ${/* child attrs */ is.objectExpression}; + /* + If 'slotAttributeValue' is set, it references a slot that does not exist, and the 'slot' attribute should be set in the DOM. This behavior aligns with engine-server and engine-dom. + See: engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/ for example case. + */ + if (slotAttributeValue) { + childAttrs.slot = slotAttributeValue; + } ${ /* Slotted content is inserted here. diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts index 6fa1660203..8ad46d5d5e 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/component/slotted-content.ts @@ -78,7 +78,7 @@ const bGenerateSlottedContent = esTemplateWithYield` const bAddSlottedContent = esTemplate` addSlottedContent(${/* slot name */ is.expression} ?? "", async function* generateSlottedContent(contextfulParent, ${ /* scoped slot data variable */ isNullableOf(is.identifier) - }) { + }, slotAttributeValue) { // FIXME: make validation work again ${/* slot content */ false} }, ${/* content map */ is.identifier}); diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/element.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/element.ts index 66341e2355..32c673fe7c 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/element.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/element.ts @@ -120,6 +120,16 @@ const bConditionallyYieldScopeTokenClass = esTemplateWithYield` } `; +/* + If `slotAttributeValue` is set, it references a slot that does not exist, and the `slot` attribute should be set in the DOM. This behavior aligns with engine-server and engine-dom. + See: engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/ for example case. +*/ +const bConditionallyYieldDanglingSlotName = esTemplateWithYield` + if (slotAttributeValue) { + yield \` slot="\${slotAttributeValue}"\`; + } +`; + const bYieldSanitizedHtml = esTemplateWithYield` yield sanitizeHtmlContent(${/* lwc:inner-html content */ is.expression}) `; @@ -263,6 +273,7 @@ export const Element: Transformer = fu return [ bYield(b.literal(`<${node.name}`)), + bConditionallyYieldDanglingSlotName(), // If we haven't already prefixed the scope token to an existing class, add an explicit class here ...(hasClassAttribute ? [] : [bConditionallyYieldScopeTokenClass()]), ...yieldAttrsAndProps, diff --git a/packages/@lwc/ssr-compiler/src/compile-template/transformers/slot.ts b/packages/@lwc/ssr-compiler/src/compile-template/transformers/slot.ts index 3dcdea4adc..952619235d 100644 --- a/packages/@lwc/ssr-compiler/src/compile-template/transformers/slot.ts +++ b/packages/@lwc/ssr-compiler/src/compile-template/transformers/slot.ts @@ -30,7 +30,13 @@ const bConditionalSlot = esTemplateWithYield` const scopedGenerators = scopedSlottedContent?.[slotName ?? ""]; const mismatchedSlots = isScopedSlot ? lightGenerators : scopedGenerators; const generators = isScopedSlot ? scopedGenerators : lightGenerators; - + /* + If a slotAttributeValue is present, it should be provided for assignment to any slotted content. This behavior aligns with v1 and engine-dom. + See: engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/ for example. + Note the slot mapping does not work for scoped slots, so the slot name is not rendered in this case. + See: engine-server/src/__tests__/fixtures/slot-forwarding/scoped-slots for example. + */ + const danglingSlotName = !isScopedSlot ? ${/* slotAttributeValue */ is.expression} || slotAttributeValue : null; // start bookend HTML comment for light DOM slot vfragment if (!isSlotted) { yield ''; @@ -43,7 +49,7 @@ const bConditionalSlot = esTemplateWithYield` if (generators) { for (let i = 0; i < generators.length; i++) { - yield* generators[i](contextfulParent, ${/* scoped slot data */ isNullableOf(is.expression)}); + yield* generators[i](contextfulParent, ${/* scoped slot data */ isNullableOf(is.expression)}, danglingSlotName); // Scoped slotted data is separated by bookends. Final bookends are added outside of the loop below. if (isScopedSlot && i < generators.length - 1) { yield ''; @@ -90,5 +96,16 @@ export const Slot: Transformer = function Slot(node, ctx): EsStatement[] const slotChildren = irChildrenToEs(node.children, ctx); const isScopedSlot = b.literal(Boolean(slotBound)); const isSlotted = b.literal(Boolean(ctx.isSlotted)); - return [bConditionalSlot(isScopedSlot, isSlotted, slotName, slotBound, slotChildren, slotAst)]; + const slotAttributeValue = bAttributeValue(node, 'slot'); + return [ + bConditionalSlot( + isScopedSlot, + isSlotted, + slotName, + slotAttributeValue, + slotBound, + slotChildren, + slotAst + ), + ]; };