diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 58076fff9ee..ff7aa56d055 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -10,6 +10,7 @@ import { insert, prepend, renderEffect, + setInsertionState, template, } from '../src' import { currentInstance, nextTick, ref } from '@vue/runtime-dom' @@ -502,5 +503,35 @@ describe('component: slots', () => { await nextTick() expect(host.innerHTML).toBe('

') }) + + test('consecutive slots with insertion state', async () => { + const { component: Child } = define({ + setup() { + const n2 = template('
baz
', true)() as any + setInsertionState(n2, 0) + createSlot('default', null) + setInsertionState(n2, 0) + createSlot('foo', null) + return n2 + }, + }) + + const { html } = define({ + setup() { + return createComponent(Child, null, { + default: () => template('default')(), + foo: () => template('foo')(), + }) + }, + }).render() + + expect(html()).toBe( + `
` + + `default` + + `foo` + + `
baz
` + + `
`, + ) + }) }) }) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index b782afd38d3..943bda67ca5 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -105,10 +105,10 @@ export function isValidBlock(block: Block): boolean { export function insert( block: Block, - parent: ParentNode, + parent: ParentNode & { $anchor?: Node | null }, anchor: Node | null | 0 = null, // 0 means prepend ): void { - anchor = anchor === 0 ? parent.firstChild : anchor + anchor = anchor === 0 ? parent.$anchor || parent.firstChild : anchor if (block instanceof Node) { if (!isHydrating) { parent.insertBefore(block, anchor) diff --git a/packages/runtime-vapor/src/dom/prop.ts b/packages/runtime-vapor/src/dom/prop.ts index 8c42ad766a5..3b663da7715 100644 --- a/packages/runtime-vapor/src/dom/prop.ts +++ b/packages/runtime-vapor/src/dom/prop.ts @@ -269,7 +269,7 @@ export function optimizePropertyLookup(): void { if (isOptimized) return isOptimized = true const proto = Element.prototype as any - proto.$evtclick = undefined + proto.$anchor = proto.$evtclick = undefined proto.$root = false proto.$html = proto.$txt = diff --git a/packages/runtime-vapor/src/insertionState.ts b/packages/runtime-vapor/src/insertionState.ts index c8c7ffbcd1d..8c66843bd93 100644 --- a/packages/runtime-vapor/src/insertionState.ts +++ b/packages/runtime-vapor/src/insertionState.ts @@ -6,7 +6,18 @@ export let insertionAnchor: Node | 0 | undefined * (component, slot outlet, if, for) is created. The state is used for actual * insertion on client-side render, and used for node adoption during hydration. */ -export function setInsertionState(parent: ParentNode, anchor?: Node | 0): void { +export function setInsertionState( + parent: ParentNode & { $anchor?: Node | null }, + anchor?: Node | 0, +): void { + // When setInsertionState(n3, 0) is called consecutively, the first prepend operation + // uses parent.firstChild as the anchor. However, after insertion, parent.firstChild + // changes and cannot serve as the anchor for subsequent prepends. Therefore, we cache + // the original parent.firstChild on the first call for subsequent prepend operations. + if (anchor === 0 && !parent.$anchor) { + parent.$anchor = parent.firstChild + } + insertionParent = parent insertionAnchor = anchor }