diff --git a/packages/compiler-vapor/src/generators/operation.ts b/packages/compiler-vapor/src/generators/operation.ts
index 4247bc6fe..a3bf5cc21 100644
--- a/packages/compiler-vapor/src/generators/operation.ts
+++ b/packages/compiler-vapor/src/generators/operation.ts
@@ -44,7 +44,7 @@ export function genOperationWithInsertionState(
): CodeFragment[] {
const [frag, push] = buildCodeFragment()
if (isBlockOperation(oper) && oper.parent) {
- push(...genInsertionstate(oper, context))
+ push(...genInsertionState(oper, context))
}
push(...genOperation(oper, context))
return frag
@@ -152,7 +152,7 @@ export function genEffect(
return frag
}
-function genInsertionstate(
+function genInsertionState(
operation: InsertionStateTypes,
context: CodegenContext,
): CodeFragment[] {
diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts
index 016872b56..14faf569c 100644
--- a/packages/runtime-vapor/__tests__/hydration.spec.ts
+++ b/packages/runtime-vapor/__tests__/hydration.spec.ts
@@ -1531,6 +1531,20 @@ describe('Vapor Mode hydration', () => {
` ` +
``,
)
+
+ data.value.splice(0, 1)
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `
` +
+ ` ` +
+ `` +
+ `b ` +
+ `c ` +
+ `d ` +
+ `` +
+ ` ` +
+ `
`,
+ )
})
test('consecutive v-for with anchor insertion', async () => {
@@ -1583,20 +1597,377 @@ describe('Vapor Mode hydration', () => {
` ` +
``,
)
+
+ data.value.splice(0, 2)
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `` +
+ ` ` +
+ `` +
+ `c ` +
+ `d ` +
+ `` +
+ `` +
+ `c ` +
+ `d ` +
+ `` +
+ ` ` +
+ `
`,
+ )
})
- // TODO wait for slots hydration support
- test.todo('v-for on component', async () => {})
+ test('v-for on component', async () => {
+ const { container, data } = await testHydration(
+ `
+
+
+
+ `,
+ {
+ Child: `comp
`,
+ },
+ ref(['a', 'b', 'c']),
+ )
- // TODO wait for slots hydration support
- test.todo('on fragment component', async () => {})
+ expect(container.innerHTML).toBe(
+ `` +
+ `` +
+ `
comp
` +
+ `
comp
` +
+ `
comp
` +
+ `` +
+ `
`,
+ )
+
+ data.value.push('d')
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `` +
+ `` +
+ `
comp
` +
+ `
comp
` +
+ `
comp
` +
+ `
comp
` +
+ `` +
+ `
`,
+ )
+ })
+
+ test('v-for on component with slots', async () => {
+ const { container, data } = await testHydration(
+ `
+
+
+ {{ item }}
+
+
+ `,
+ {
+ Child: ` `,
+ },
+ ref(['a', 'b', 'c']),
+ )
+ expect(container.innerHTML).toBe(
+ `` +
+ `` +
+ `a ` +
+ `b ` +
+ `c ` +
+ `` +
+ `
`,
+ )
+
+ data.value.push('d')
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `` +
+ `` +
+ `a ` +
+ `b ` +
+ `c ` +
+ `d ` +
+ `` +
+ `
`,
+ )
+ })
+
+ test('on fragment component', async () => {
+ const { container, data } = await testHydration(
+ `
+
+
+
+ `,
+ {
+ Child: `foo
-bar- `,
+ },
+ ref(['a', 'b', 'c']),
+ )
+ expect(container.innerHTML).toBe(
+ `` +
+ `` +
+ `
foo
-bar-` +
+ `
foo
-bar-` +
+ `
foo
-bar-` +
+ `` +
+ `
`,
+ )
+
+ data.value.push('d')
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `` +
+ `` +
+ `
foo
-bar-` +
+ `
foo
-bar-` +
+ `
foo
-bar-` +
+ `
foo
-bar-` +
+ `` +
+ `
`,
+ )
+ })
// TODO wait for vapor TransitionGroup support
// v-for inside TransitionGroup does not render as a fragment
test.todo('v-for in TransitionGroup', async () => {})
})
- test.todo('slots')
+ describe('slots', () => {
+ test('basic slot', async () => {
+ const { data, container } = await testHydration(
+ `
+
+ {{data}}
+
+ `,
+ {
+ Child: ` `,
+ },
+ )
+ expect(container.innerHTML).toBe(
+ `foo `,
+ )
+
+ data.value = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `bar `,
+ )
+ })
+
+ test('named slot', async () => {
+ const { data, container } = await testHydration(
+ `
+
+
+ {{data}}
+
+
+ `,
+ {
+ Child: ` `,
+ },
+ )
+ expect(container.innerHTML).toBe(
+ `foo `,
+ )
+
+ data.value = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `bar `,
+ )
+ })
+
+ test('named slot with v-if', async () => {
+ const { data, container } = await testHydration(
+ `
+
+
+ {{data}}
+
+
+ `,
+ {
+ Child: ` `,
+ },
+ )
+ expect(container.innerHTML).toBe(
+ `foo `,
+ )
+
+ data.value = false
+ await nextTick()
+ expect(container.innerHTML).toBe(``)
+ })
+
+ test('named slot with v-if and v-for', async () => {
+ const data = reactive({
+ show: true,
+ items: ['a', 'b', 'c'],
+ })
+ const { container } = await testHydration(
+ `
+
+
+ {{item}}
+
+
+ `,
+ {
+ Child: ` `,
+ },
+ data,
+ )
+ expect(container.innerHTML).toBe(
+ `` +
+ `a b c ` +
+ `` +
+ ``,
+ )
+
+ data.show = false
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ ``,
+ )
+ })
+
+ test('with anchor insertion', async () => {
+ const { data, container } = await testHydration(
+ `
+
+
+ {{data}}
+
+
+ `,
+ {
+ Child: ` `,
+ },
+ )
+ expect(container.innerHTML).toBe(
+ `` +
+ ` ` +
+ `foo ` +
+ ` ` +
+ `` +
+ ``,
+ )
+
+ data.value = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `` +
+ ` ` +
+ `bar ` +
+ ` ` +
+ `` +
+ ``,
+ )
+ })
+
+ test('with multi level anchor insertion', async () => {
+ const { data, container } = await testHydration(
+ `
+
+
+ {{data}}
+
+
+ `,
+ {
+ Child: `
+
+
+
+
+
+
+ `,
+ },
+ )
+ expect(container.innerHTML).toBe(
+ `` +
+ `
` +
+ `
` +
+ `` +
+ ` ` +
+ `foo ` +
+ ` ` +
+ `` +
+ `` +
+ `
` +
+ ``,
+ )
+
+ data.value = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `` +
+ `
` +
+ `
` +
+ `` +
+ ` ` +
+ `bar ` +
+ ` ` +
+ `` +
+ `` +
+ `
` +
+ ``,
+ )
+ })
+
+ // problem is next child is incorrect after slot
+ test.todo('mixed slot and text node', async () => {
+ const data = reactive({
+ text: 'foo',
+ msg: 'hi',
+ })
+ const { container } = await testHydration(
+ `
+
+ {{data.text}}
+
+ `,
+ {
+ Child: ` {{data.msg}}
`,
+ },
+ data,
+ )
+
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"foo hi
"`,
+ )
+ })
+
+ test.todo('mixed slot and element', async () => {
+ const data = reactive({
+ text: 'foo',
+ msg: 'hi',
+ })
+ const { container } = await testHydration(
+ `
+
+ {{data.text}}
+
+ `,
+ {
+ Child: ` `,
+ },
+ data,
+ )
+
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `""`,
+ )
+ })
+
+ // mixed slot and component
+ // mixed slot and fragment component
+ // mixed slot and v-if
+ // mixed slot and v-for
+ })
// test('element with ref', () => {
// const el = ref()
diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts
index c0727cb5d..c846cc872 100644
--- a/packages/runtime-vapor/src/block.ts
+++ b/packages/runtime-vapor/src/block.ts
@@ -1,4 +1,4 @@
-import { isArray, isVaporFragmentEndAnchor } from '@vue/shared'
+import { isArray } from '@vue/shared'
import {
type VaporComponentInstance,
isVaporComponent,
@@ -100,10 +100,10 @@ export class DynamicFragment extends VaporFragment {
} else {
// find next sibling dynamic fragment end anchor
const anchor = nextVaporFragmentAnchor(currentHydrationNode!, label)!
- if (anchor && isVaporFragmentEndAnchor(anchor)) {
+ if (anchor) {
this.anchor = anchor
} else if (__DEV__) {
- // TODO warning
+ // TODO warning, should not happen
warn(`DynamicFragment anchor not found...`)
}
}
diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts
index 74296e094..093ed7fb0 100644
--- a/packages/runtime-vapor/src/componentSlots.ts
+++ b/packages/runtime-vapor/src/componentSlots.ts
@@ -1,11 +1,22 @@
-import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared'
+import {
+ EMPTY_OBJ,
+ NO,
+ SLOT_ANCHOR_LABEL,
+ hasOwn,
+ isArray,
+ isFunction,
+} from '@vue/shared'
import { type Block, type BlockFn, DynamicFragment, insert } from './block'
import { rawPropsProxyHandlers } from './componentProps'
import { currentInstance, isRef } from '@vue/runtime-dom'
import type { LooseRawProps, VaporComponentInstance } from './component'
import { renderEffect } from './renderEffect'
-import { insertionAnchor, insertionParent } from './insertionState'
-import { isHydrating, locateHydrationNode } from './dom/hydration'
+import {
+ insertionAnchor,
+ insertionParent,
+ resetInsertionState,
+} from './insertionState'
+import { isHydrating } from './dom/hydration'
export type RawSlots = Record & {
$?: DynamicSlotSource[]
@@ -94,9 +105,7 @@ export function createSlot(
): Block {
const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor
- if (isHydrating) {
- locateHydrationNode()
- }
+ if (!isHydrating) resetInsertionState()
const instance = currentInstance as VaporComponentInstance
const rawSlots = instance.rawSlots
@@ -115,7 +124,7 @@ export function createSlot(
fallback,
)
} else {
- fragment = __DEV__ ? new DynamicFragment('slot') : new DynamicFragment()
+ fragment = new DynamicFragment(SLOT_ANCHOR_LABEL)
const isDynamicName = isFunction(name)
const renderSlot = () => {
const slot = getSlot(rawSlots, isFunction(name) ? name() : name)
diff --git a/packages/runtime-vapor/src/dom/hydration.ts b/packages/runtime-vapor/src/dom/hydration.ts
index f290954b7..2506a5c8f 100644
--- a/packages/runtime-vapor/src/dom/hydration.ts
+++ b/packages/runtime-vapor/src/dom/hydration.ts
@@ -42,7 +42,7 @@ export function withHydration(container: ParentNode, fn: () => void): void {
}
export let adoptTemplate: (node: Node, template: string) => Node | null
-export let locateHydrationNode: (isFragment?: boolean) => void
+export let locateHydrationNode: (hasFragmentAnchor?: boolean) => void
type Anchor = Comment & {
// cached matching fragment start to avoid repeated traversal
@@ -94,10 +94,16 @@ function locateHydrationNodeImpl(hasFragmentAnchor?: boolean) {
} else {
node = insertionParent ? insertionParent.lastChild : currentHydrationNode
+ // if current node is fragment start anchor, find the next one
+ if (node && isComment(node, '[')) {
+ node = node.nextSibling
+ }
// if the last child is a vapor fragment end anchor, find the previous one
- if (hasFragmentAnchor && node && isVaporFragmentEndAnchor(node)) {
- let previous = node.previousSibling
- if (previous) node = previous
+ else if (hasFragmentAnchor && node && isVaporFragmentEndAnchor(node)) {
+ node = node.previousSibling
+ if (__DEV__ && !node) {
+ // TODO warning, should not happen
+ }
}
if (node && isComment(node, ']')) {
diff --git a/packages/server-renderer/__tests__/render.spec.ts b/packages/server-renderer/__tests__/render.spec.ts
index d0a5223b2..03b7402ff 100644
--- a/packages/server-renderer/__tests__/render.spec.ts
+++ b/packages/server-renderer/__tests__/render.spec.ts
@@ -446,7 +446,7 @@ function testRender(type: string, render: typeof renderToString) {
).toBe(
`parent
` +
`from slot ` +
- `
`,
+ ``,
)
// test fallback
@@ -461,7 +461,7 @@ function testRender(type: string, render: typeof renderToString) {
}),
),
).toBe(
- ``,
+ ``,
)
})
@@ -507,7 +507,7 @@ function testRender(type: string, render: typeof renderToString) {
).toBe(
`parent
` +
`from slot ` +
- `
`,
+ ``,
)
})
@@ -525,7 +525,7 @@ function testRender(type: string, render: typeof renderToString) {
expect(await render(app)).toBe(
`parent
` +
`from slot ` +
- `
`,
+ ``,
)
})
@@ -572,7 +572,7 @@ function testRender(type: string, render: typeof renderToString) {
})
expect(await render(app)).toBe(
- `hello
`,
+ `hello
`,
)
})
@@ -593,7 +593,7 @@ function testRender(type: string, render: typeof renderToString) {
expect(await render(app)).toBe(
// should only have a single fragment
- `
`,
+ `
`,
)
})
@@ -614,7 +614,7 @@ function testRender(type: string, render: typeof renderToString) {
expect(await render(app)).toBe(
// should only have a single fragment
- `fallback
`,
+ `fallback
`,
)
})
})
diff --git a/packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts b/packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts
index b685fbfe1..181720c5b 100644
--- a/packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts
+++ b/packages/server-renderer/__tests__/ssrDynamicComponent.spec.ts
@@ -15,7 +15,7 @@ describe('ssr: dynamic component', () => {
}),
),
).toBe(
- `slot
`,
+ `slot
`,
)
})
@@ -63,7 +63,7 @@ describe('ssr: dynamic component', () => {
}),
),
).toBe(
- `testslot
`,
+ `testslot
`,
)
})
diff --git a/packages/server-renderer/__tests__/ssrScopeId.spec.ts b/packages/server-renderer/__tests__/ssrScopeId.spec.ts
index 4ceb865fb..c4135e498 100644
--- a/packages/server-renderer/__tests__/ssrScopeId.spec.ts
+++ b/packages/server-renderer/__tests__/ssrScopeId.spec.ts
@@ -68,7 +68,9 @@ describe('ssr: scopedId runtime behavior', () => {
}
const result = await renderToString(createApp(Comp))
- expect(result).toBe(`
`)
+ expect(result).toBe(
+ `
`,
+ )
})
// #2892
@@ -150,8 +152,8 @@ describe('ssr: scopedId runtime behavior', () => {
const result = await renderToString(createApp(Root))
expect(result).toBe(
``,
+ `
` +
+ ``,
)
})
@@ -265,8 +267,8 @@ describe('ssr: scopedId runtime behavior', () => {
const result = await renderToString(createApp(Root))
expect(result).toBe(
``,
+ `
` +
+ ``,
)
})
})
diff --git a/packages/server-renderer/__tests__/ssrSlot.spec.ts b/packages/server-renderer/__tests__/ssrSlot.spec.ts
index 0e3e35356..d17e34bc7 100644
--- a/packages/server-renderer/__tests__/ssrSlot.spec.ts
+++ b/packages/server-renderer/__tests__/ssrSlot.spec.ts
@@ -16,7 +16,7 @@ describe('ssr: slot', () => {
template: `hello `,
}),
),
- ).toBe(`hello
`)
+ ).toBe(`hello
`)
})
test('element slot', async () => {
@@ -27,7 +27,7 @@ describe('ssr: slot', () => {
template: `hi
`,
}),
),
- ).toBe(``)
+ ).toBe(``)
})
test('empty slot', async () => {
@@ -42,7 +42,7 @@ describe('ssr: slot', () => {
template: ` `,
}),
),
- ).toBe(`
`)
+ ).toBe(`
`)
})
test('empty slot (manual comments)', async () => {
@@ -57,7 +57,7 @@ describe('ssr: slot', () => {
template: ` `,
}),
),
- ).toBe(`
`)
+ ).toBe(`
`)
})
test('empty slot (multi-line comments)', async () => {
@@ -72,7 +72,7 @@ describe('ssr: slot', () => {
template: ` `,
}),
),
- ).toBe(`
`)
+ ).toBe(`
`)
})
test('multiple elements', async () => {
@@ -83,7 +83,7 @@ describe('ssr: slot', () => {
template: `one
two
`,
}),
),
- ).toBe(``)
+ ).toBe(``)
})
test('fragment slot (template v-if)', async () => {
@@ -94,7 +94,9 @@ describe('ssr: slot', () => {
template: `hello `,
}),
),
- ).toBe(`hello
`)
+ ).toBe(
+ `hello
`,
+ )
})
test('fragment slot (template v-if + multiple elements)', async () => {
@@ -106,7 +108,7 @@ describe('ssr: slot', () => {
}),
),
).toBe(
- ``,
+ ``,
)
})
@@ -171,7 +173,7 @@ describe('ssr: slot', () => {
}),
),
).toBe(
- `
`,
+ `
`,
)
expect(
@@ -189,7 +191,7 @@ describe('ssr: slot', () => {
}),
),
).toBe(
- ` `,
+ ` `,
)
expect(
diff --git a/packages/server-renderer/src/helpers/ssrRenderSlot.ts b/packages/server-renderer/src/helpers/ssrRenderSlot.ts
index 19aa4ce63..b8a57ae8d 100644
--- a/packages/server-renderer/src/helpers/ssrRenderSlot.ts
+++ b/packages/server-renderer/src/helpers/ssrRenderSlot.ts
@@ -5,7 +5,7 @@ import {
type SSRBufferItem,
renderVNodeChildren,
} from '../render'
-import { isArray } from '@vue/shared'
+import { SLOT_ANCHOR_LABEL, isArray } from '@vue/shared'
const { ensureValidVNode } = ssrUtils
@@ -37,7 +37,7 @@ export function ssrRenderSlot(
parentComponent,
slotScopeId,
)
- push(``)
+ push(``)
}
export function ssrRenderSlotInner(