diff --git a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts
index 53295fb03..73d4331a7 100644
--- a/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts
@@ -15,7 +15,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`
\`)
})
- _push(\`\`)
+ _push(\`\`)
}"
`)
})
@@ -33,7 +33,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`\`)
})
- _push(\`\`)
+ _push(\`\`)
}"
`)
})
@@ -52,6 +52,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`\`)
})
+ _push(\`\`)
if (false) {
_push(\`\`)
_push(\`\`)
@@ -75,7 +76,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`\`)
})
- _push(\`\`)
+ _push(\`\`)
}"
`)
})
@@ -97,7 +98,7 @@ describe('transition-group', () => {
_ssrRenderList(_ctx.list, (i) => {
_push(\`\`)
})
- _push(\`\${_ctx.someTag}>\`)
+ _push(\`\${_ctx.someTag}>\`)
}"
`)
})
@@ -119,9 +120,11 @@ describe('transition-group', () => {
_ssrRenderList(10, (i) => {
_push(\`\`)
})
+ _push(\`\`)
_ssrRenderList(10, (i) => {
_push(\`\`)
})
+ _push(\`\`)
if (_ctx.ok) {
_push(\`ok
\`)
_push(\`\`)
diff --git a/packages/compiler-ssr/src/transforms/ssrVFor.ts b/packages/compiler-ssr/src/transforms/ssrVFor.ts
index 6537eee82..827650785 100644
--- a/packages/compiler-ssr/src/transforms/ssrVFor.ts
+++ b/packages/compiler-ssr/src/transforms/ssrVFor.ts
@@ -13,6 +13,7 @@ import {
processChildrenAsStatement,
} from '../ssrCodegenTransform'
import { SSR_RENDER_LIST } from '../runtimeHelpers'
+import { FOR_ANCHOR_LABEL } from '@vue/shared'
// Plugin for the first transform pass, which simply constructs the AST node
export const ssrTransformFor: NodeTransform =
@@ -48,5 +49,8 @@ export function ssrProcessFor(
)
if (!disableNestedFragments) {
context.pushStringPart(``)
+ } else {
+ // add anchor for non-fragment v-for
+ context.pushStringPart(``)
}
}
diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts
index a37076ce1..c15bd6854 100644
--- a/packages/runtime-vapor/__tests__/hydration.spec.ts
+++ b/packages/runtime-vapor/__tests__/hydration.spec.ts
@@ -1123,6 +1123,73 @@ describe('Vapor Mode hydration', () => {
})
})
+ test('on fragment component', async () => {
+ runWithEnv(isProd, async () => {
+ const data = ref(true)
+ const { container } = await testHydration(
+ `
+
+
+
+ `,
+ {
+ Child: `{{ data }}
-{{ data }}-`,
+ },
+ data,
+ )
+ expect(container.innerHTML).toBe(
+ `` +
+ `
true
-true-` +
+ `` +
+ `
`,
+ )
+
+ data.value = false
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `` + `` + `` + `
`,
+ )
+ })
+ })
+
+ test('on fragment component with anchor insertion', async () => {
+ runWithEnv(isProd, async () => {
+ const data = ref(true)
+ const { container } = await testHydration(
+ `
+
+
+
+
+
+ `,
+ {
+ Child: `{{ data }}
-{{ data }}-`,
+ },
+ data,
+ )
+ expect(container.innerHTML).toBe(
+ `` +
+ `
` +
+ `
true
-true-` +
+ `` +
+ `
` +
+ `
`,
+ )
+
+ data.value = false
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `` +
+ `` +
+ `` +
+ `` +
+ `` +
+ `
`,
+ )
+ })
+ })
+
test('consecutive v-if on fragment component with anchor insertion', async () => {
runWithEnv(isProd, async () => {
const data = ref(true)
@@ -1311,7 +1378,168 @@ describe('Vapor Mode hydration', () => {
}
})
- test.todo('for')
+ describe('for', () => {
+ test('basic v-for', async () => {
+ const { container, data } = await testHydration(
+ `
+
+ {{ item }}
+
+ `,
+ undefined,
+ 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('v-for with text node', async () => {
+ const { container, data } = await testHydration(
+ `
+
+ {{ item }}
+
+ `,
+ undefined,
+ ref(['a', 'b', 'c']),
+ )
+ expect(container.innerHTML).toBe(
+ `abc
`,
+ )
+
+ data.value.push('d')
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `abcd
`,
+ )
+ })
+
+ test('v-for with anchor insertion', async () => {
+ const { container, data } = await testHydration(
+ `
+
+
+ {{ item }}
+
+
+ `,
+ undefined,
+ 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('consecutive v-for with anchor insertion', async () => {
+ const { container, data } = await testHydration(
+ `
+
+
+ {{ item }}
+ {{ item }}
+
+
+ `,
+ undefined,
+ ref(['a', 'b', 'c']),
+ )
+ expect(container.innerHTML).toBe(
+ `` +
+ `` +
+ `` +
+ `a` +
+ `b` +
+ `c` +
+ `` +
+ `` +
+ `` +
+ `a` +
+ `b` +
+ `c` +
+ `` +
+ `` +
+ `` +
+ `
`,
+ )
+
+ data.value.push('d')
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `` +
+ `` +
+ `` +
+ `a` +
+ `b` +
+ `c` +
+ `d` +
+ `` +
+ `` +
+ `` +
+ `a` +
+ `b` +
+ `c` +
+ `d` +
+ `` +
+ `` +
+ `` +
+ `
`,
+ )
+ })
+
+ // TODO wait for slots hydration support
+ test.todo('v-for on component', async () => {})
+
+ // TODO wait for slots hydration support
+ test.todo('on fragment component', async () => {})
+
+ // 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')
diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts
index 0cd831753..b8016a64d 100644
--- a/packages/runtime-vapor/src/apiCreateFor.ts
+++ b/packages/runtime-vapor/src/apiCreateFor.ts
@@ -9,8 +9,14 @@ import {
shallowRef,
toReactive,
} from '@vue/reactivity'
-import { getSequence, isArray, isObject, isString } from '@vue/shared'
-import { createComment, createTextNode } from './dom/node'
+import {
+ FOR_ANCHOR_LABEL,
+ getSequence,
+ isArray,
+ isObject,
+ isString,
+} from '@vue/shared'
+import { createComment, createTextNode, nextSiblingAnchor } from './dom/node'
import {
type Block,
VaporFragment,
@@ -22,8 +28,17 @@ import { currentInstance, isVaporComponent } from './component'
import type { DynamicSlot } from './componentSlots'
import { renderEffect } from './renderEffect'
import { VaporVForFlags } from '../../shared/src/vaporFlags'
-import { isHydrating, locateHydrationNode } from './dom/hydration'
-import { insertionAnchor, insertionParent } from './insertionState'
+import {
+ currentHydrationNode,
+ isComment,
+ isHydrating,
+ locateHydrationNode,
+} from './dom/hydration'
+import {
+ insertionAnchor,
+ insertionParent,
+ resetInsertionState,
+} from './insertionState'
class ForBlock extends VaporFragment {
scope: EffectScope | undefined
@@ -71,15 +86,24 @@ export const createFor = (
const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor
if (isHydrating) {
- locateHydrationNode()
+ locateHydrationNode(true)
+ } else {
+ resetInsertionState()
}
let isMounted = false
let oldBlocks: ForBlock[] = []
let newBlocks: ForBlock[]
let parent: ParentNode | undefined | null
- // TODO handle this in hydration
- const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
+ const parentAnchor = isHydrating
+ ? // Use fragment end anchor if available, otherwise use the specific for anchor.
+ nextSiblingAnchor(
+ currentHydrationNode!,
+ isComment(currentHydrationNode!, '[') ? ']' : FOR_ANCHOR_LABEL,
+ )!
+ : __DEV__
+ ? createComment('for')
+ : createTextNode()
const frag = new VaporFragment(oldBlocks)
const instance = currentInstance!
const canUseFastRemove = flags & VaporVForFlags.FAST_REMOVE
diff --git a/packages/runtime-vapor/src/dom/hydration.ts b/packages/runtime-vapor/src/dom/hydration.ts
index b0e8e7d52..4588f4887 100644
--- a/packages/runtime-vapor/src/dom/hydration.ts
+++ b/packages/runtime-vapor/src/dom/hydration.ts
@@ -10,7 +10,6 @@ import {
disableHydrationNodeLookup,
enableHydrationNodeLookup,
next,
- prev,
} from './node'
import { isDynamicFragmentEndAnchor } from '@vue/shared'
@@ -98,7 +97,7 @@ function locateHydrationNodeImpl(isFragment?: boolean) {
// if the last child is a comment, it is the anchor for the fragment
// so it need to find the previous node
if (isFragment && node && isDynamicFragmentEndAnchor(node)) {
- let previous = prev(node)
+ let previous = node.previousSibling //prev(node)
if (previous) node = previous
}
diff --git a/packages/runtime-vapor/src/dom/node.ts b/packages/runtime-vapor/src/dom/node.ts
index 2a546c69e..6b9b63c5c 100644
--- a/packages/runtime-vapor/src/dom/node.ts
+++ b/packages/runtime-vapor/src/dom/node.ts
@@ -105,6 +105,7 @@ export function disableHydrationNodeLookup(): void {
}
/*! #__NO_SIDE_EFFECTS__ */
+// TODO check if this is still needed
export function prev(node: Node): Node | null {
// process dynamic node (...) as a single one
if (isComment(node, DYNAMIC_END_ANCHOR_LABEL)) {
@@ -145,6 +146,9 @@ export function nextSiblingAnchor(
anchorLabel: string,
): Comment | null {
node = handleWrappedNode(node)
+ if (isComment(node, anchorLabel)) {
+ return node as Comment
+ }
let n = node.nextSibling
while (n) {