diff --git a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts index 97f68101f74..e55da38e0f6 100644 --- a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts @@ -759,6 +759,63 @@ describe('compiler: transform component slots', () => { expect(generate(root).code).toMatchSnapshot() }) + // #14425 + test('conditional slot between static slots preserves template order', () => { + const { slots } = parseWithSlots( + ` + + + + `, + ) + expect(slots).toMatchObject({ + type: NodeTypes.JS_CALL_EXPRESSION, + callee: CREATE_SLOTS, + arguments: [ + createObjectMatcher({ + foo: { + type: NodeTypes.JS_FUNCTION_EXPRESSION, + returns: [{ type: NodeTypes.TEXT, content: `foo` }], + }, + bar: { + type: NodeTypes.JS_FUNCTION_EXPRESSION, + returns: [{ type: NodeTypes.TEXT, content: `bar` }], + }, + _: `[2 /* DYNAMIC */]`, + }), + { + type: NodeTypes.JS_ARRAY_EXPRESSION, + elements: [ + { + type: NodeTypes.JS_CONDITIONAL_EXPRESSION, + test: { content: `ok` }, + consequent: createObjectMatcher({ + name: `baz`, + fn: { + type: NodeTypes.JS_FUNCTION_EXPRESSION, + returns: [{ type: NodeTypes.TEXT, content: `baz` }], + }, + key: `0`, + }), + alternate: { + content: `undefined`, + isStatic: false, + }, + }, + ], + }, + { + type: NodeTypes.JS_ARRAY_EXPRESSION, + elements: [ + { content: `foo`, isStatic: true }, + { content: `baz`, isStatic: true }, + { content: `bar`, isStatic: true }, + ], + }, + ], + }) + }) + test('named slot with v-for w/ prefixIdentifiers: true', () => { const { root, slots } = parseWithSlots( ` diff --git a/packages/compiler-core/src/transforms/vSlot.ts b/packages/compiler-core/src/transforms/vSlot.ts index f9a1b72daad..92bbad685e7 100644 --- a/packages/compiler-core/src/transforms/vSlot.ts +++ b/packages/compiler-core/src/transforms/vSlot.ts @@ -128,6 +128,7 @@ export function buildSlots( const { children, loc } = node const slotsProperties: Property[] = [] const dynamicSlots: (ConditionalExpression | CallExpression)[] = [] + const slotOrder: string[] = [] // If the slot is inside a v-for or another v-slot, force it to be dynamic // since it likely uses a scope variable. @@ -217,6 +218,7 @@ export function buildSlots( let vElse: DirectiveNode | undefined if ((vIf = findDir(slotElement, 'if'))) { hasDynamicSlots = true + if (staticSlotName) slotOrder.push(staticSlotName) dynamicSlots.push( createConditionalExpression( vIf.exp!, @@ -238,6 +240,7 @@ export function buildSlots( } if (prev && isTemplateNode(prev) && findDir(prev, /^(?:else-)?if$/)) { __TEST__ && assert(dynamicSlots.length > 0) + if (staticSlotName) slotOrder.push(staticSlotName) // attach this slot to previous conditional let conditional = dynamicSlots[ dynamicSlots.length - 1 @@ -305,6 +308,8 @@ export function buildSlots( hasNamedDefaultSlot = true } } + + if (staticSlotName) slotOrder.push(staticSlotName) slotsProperties.push(createObjectProperty(slotName, slotFunction)) } } @@ -366,11 +371,26 @@ export function buildSlots( ), loc, ) as SlotsExpression - if (dynamicSlots.length) { - slots = createCallExpression(context.helper(CREATE_SLOTS), [ + + if (dynamicSlots.length > 0) { + const createSlotsArgs: CallExpression['arguments'] = [ slots, createArrayExpression(dynamicSlots), - ]) as SlotsExpression + ] + // #14425 + // Pass slot names to preserve the template ordering + if (slotsProperties.length > 0) { + createSlotsArgs.push( + createArrayExpression( + slotOrder.map(name => createSimpleExpression(name, true)), + ), + ) + } + + slots = createCallExpression( + context.helper(CREATE_SLOTS), + createSlotsArgs, + ) as SlotsExpression } return { diff --git a/packages/runtime-core/__tests__/componentSlots.spec.ts b/packages/runtime-core/__tests__/componentSlots.spec.ts index 458731dd150..91a2d1eaadc 100644 --- a/packages/runtime-core/__tests__/componentSlots.spec.ts +++ b/packages/runtime-core/__tests__/componentSlots.spec.ts @@ -461,4 +461,51 @@ describe('component: slots', () => { createApp(App).mount(root) expect(serializeInner(root)).toBe('foo') }) + + // #14425 + test('conditionally rendered slot position in `slots` instance property should match its position in template', async () => { + const showFoo = ref(true) + + let instance: any + const Child = () => { + instance = getCurrentInstance() + return 'child' + } + + const Comp = { + setup() { + return () => [ + h( + Child, + null, + createSlots( + { + bar: () => [h('span', 'bar')], + baz: () => [h('span', 'baz')], + // @ts-expect-error property holding slots flag DYNAMIC + _: 2, + }, + [ + showFoo.value + ? { name: 'foo', fn: () => [h('span', 'foo')] } + : undefined, + ], + ['foo', 'bar', 'baz'], + ), + ), + ] + }, + } + + render(h(Comp), nodeOps.createElement('div')) + expect(Object.keys(instance.slots)).toEqual(['foo', 'bar', 'baz']) + + showFoo.value = false + await nextTick() + expect(Object.keys(instance.slots)).toEqual(['bar', 'baz']) + + showFoo.value = true + await nextTick() + expect(Object.keys(instance.slots)).toEqual(['foo', 'bar', 'baz']) + }) }) diff --git a/packages/runtime-core/__tests__/helpers/createSlots.spec.ts b/packages/runtime-core/__tests__/helpers/createSlots.spec.ts index 85018854eae..b1949e66eaf 100644 --- a/packages/runtime-core/__tests__/helpers/createSlots.spec.ts +++ b/packages/runtime-core/__tests__/helpers/createSlots.spec.ts @@ -73,4 +73,65 @@ describe('createSlot', () => { descriptor3: slot, }) }) + + describe('order parameter', () => { + it('should treat duplicate slot names as a no-op (v-if/v-else branches)', () => { + record = { default: slot } + + const actual = createSlots( + record, + [ + { name: 'header', fn: slot, key: '0' }, + { name: 'header', fn: slot, key: '1' }, + ], + ['header', 'header'], + ) + + expect(Object.keys(actual)).toEqual(['default', 'header']) + expect(actual).toHaveProperty('header') + }) + + // Simulates `