Skip to content

Commit e6e0160

Browse files
committed
wip: v-for hydration
1 parent aad75fd commit e6e0160

File tree

6 files changed

+276
-14
lines changed

6 files changed

+276
-14
lines changed

packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe('transition-group', () => {
1515
_ssrRenderList(_ctx.list, (i) => {
1616
_push(\`<div></div>\`)
1717
})
18-
_push(\`<!--]-->\`)
18+
_push(\`<!--for--><!--]-->\`)
1919
}"
2020
`)
2121
})
@@ -33,7 +33,7 @@ describe('transition-group', () => {
3333
_ssrRenderList(_ctx.list, (i) => {
3434
_push(\`<div></div>\`)
3535
})
36-
_push(\`</ul>\`)
36+
_push(\`<!--for--></ul>\`)
3737
}"
3838
`)
3939
})
@@ -52,6 +52,7 @@ describe('transition-group', () => {
5252
_ssrRenderList(_ctx.list, (i) => {
5353
_push(\`<div></div>\`)
5454
})
55+
_push(\`<!--for-->\`)
5556
if (false) {
5657
_push(\`<div></div>\`)
5758
_push(\`<!--if-->\`)
@@ -75,7 +76,7 @@ describe('transition-group', () => {
7576
_ssrRenderList(_ctx.list, (i) => {
7677
_push(\`<div></div>\`)
7778
})
78-
_push(\`</ul>\`)
79+
_push(\`<!--for--></ul>\`)
7980
}"
8081
`)
8182
})
@@ -97,7 +98,7 @@ describe('transition-group', () => {
9798
_ssrRenderList(_ctx.list, (i) => {
9899
_push(\`<div></div>\`)
99100
})
100-
_push(\`</\${_ctx.someTag}>\`)
101+
_push(\`<!--for--></\${_ctx.someTag}>\`)
101102
}"
102103
`)
103104
})
@@ -119,9 +120,11 @@ describe('transition-group', () => {
119120
_ssrRenderList(10, (i) => {
120121
_push(\`<div></div>\`)
121122
})
123+
_push(\`<!--for-->\`)
122124
_ssrRenderList(10, (i) => {
123125
_push(\`<div></div>\`)
124126
})
127+
_push(\`<!--for-->\`)
125128
if (_ctx.ok) {
126129
_push(\`<div>ok</div>\`)
127130
_push(\`<!--if-->\`)

packages/compiler-ssr/src/transforms/ssrVFor.ts

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
processChildrenAsStatement,
1414
} from '../ssrCodegenTransform'
1515
import { SSR_RENDER_LIST } from '../runtimeHelpers'
16+
import { FOR_ANCHOR_LABEL } from '@vue/shared'
1617

1718
// Plugin for the first transform pass, which simply constructs the AST node
1819
export const ssrTransformFor: NodeTransform =
@@ -48,5 +49,8 @@ export function ssrProcessFor(
4849
)
4950
if (!disableNestedFragments) {
5051
context.pushStringPart(`<!--]-->`)
52+
} else {
53+
// add anchor for non-fragment v-for
54+
context.pushStringPart(`<!--${FOR_ANCHOR_LABEL}-->`)
5155
}
5256
}

packages/runtime-vapor/__tests__/hydration.spec.ts

+229-1
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,73 @@ describe('Vapor Mode hydration', () => {
11231123
})
11241124
})
11251125

1126+
test('on fragment component', async () => {
1127+
runWithEnv(isProd, async () => {
1128+
const data = ref(true)
1129+
const { container } = await testHydration(
1130+
`<template>
1131+
<div>
1132+
<components.Child v-if="data"/>
1133+
</div>
1134+
</template>`,
1135+
{
1136+
Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
1137+
},
1138+
data,
1139+
)
1140+
expect(container.innerHTML).toBe(
1141+
`<div>` +
1142+
`<!--[--><div>true</div>-true-<!--]-->` +
1143+
`<!--if-->` +
1144+
`</div>`,
1145+
)
1146+
1147+
data.value = false
1148+
await nextTick()
1149+
expect(container.innerHTML).toBe(
1150+
`<div>` + `<!--[--><!--]-->` + `<!--${anchorLabel}-->` + `</div>`,
1151+
)
1152+
})
1153+
})
1154+
1155+
test('on fragment component with anchor insertion', async () => {
1156+
runWithEnv(isProd, async () => {
1157+
const data = ref(true)
1158+
const { container } = await testHydration(
1159+
`<template>
1160+
<div>
1161+
<span/>
1162+
<components.Child v-if="data"/>
1163+
<span/>
1164+
</div>
1165+
</template>`,
1166+
{
1167+
Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
1168+
},
1169+
data,
1170+
)
1171+
expect(container.innerHTML).toBe(
1172+
`<div>` +
1173+
`<span></span>` +
1174+
`<!--[--><div>true</div>-true-<!--]-->` +
1175+
`<!--if-->` +
1176+
`<span></span>` +
1177+
`</div>`,
1178+
)
1179+
1180+
data.value = false
1181+
await nextTick()
1182+
expect(container.innerHTML).toBe(
1183+
`<div>` +
1184+
`<span></span>` +
1185+
`<!--[--><!--]-->` +
1186+
`<!--${anchorLabel}-->` +
1187+
`<span></span>` +
1188+
`</div>`,
1189+
)
1190+
})
1191+
})
1192+
11261193
test('consecutive v-if on fragment component with anchor insertion', async () => {
11271194
runWithEnv(isProd, async () => {
11281195
const data = ref(true)
@@ -1311,7 +1378,168 @@ describe('Vapor Mode hydration', () => {
13111378
}
13121379
})
13131380

1314-
test.todo('for')
1381+
describe('for', () => {
1382+
test('basic v-for', async () => {
1383+
const { container, data } = await testHydration(
1384+
`<template>
1385+
<div>
1386+
<span v-for="item in data" :key="item">{{ item }}</span>
1387+
</div>
1388+
</template>`,
1389+
undefined,
1390+
ref(['a', 'b', 'c']),
1391+
)
1392+
expect(container.innerHTML).toBe(
1393+
`<div>` +
1394+
`<!--[-->` +
1395+
`<span>a</span>` +
1396+
`<span>b</span>` +
1397+
`<span>c</span>` +
1398+
`<!--]-->` +
1399+
`</div>`,
1400+
)
1401+
1402+
data.value.push('d')
1403+
await nextTick()
1404+
expect(container.innerHTML).toBe(
1405+
`<div>` +
1406+
`<!--[-->` +
1407+
`<span>a</span>` +
1408+
`<span>b</span>` +
1409+
`<span>c</span>` +
1410+
`<span>d</span>` +
1411+
`<!--]-->` +
1412+
`</div>`,
1413+
)
1414+
})
1415+
1416+
test('v-for with text node', async () => {
1417+
const { container, data } = await testHydration(
1418+
`<template>
1419+
<div>
1420+
<span v-for="item in data" :key="item">{{ item }}</span>
1421+
</div>
1422+
</template>`,
1423+
undefined,
1424+
ref(['a', 'b', 'c']),
1425+
)
1426+
expect(container.innerHTML).toBe(
1427+
`<div><!--[--><span>a</span><span>b</span><span>c</span><!--]--></div>`,
1428+
)
1429+
1430+
data.value.push('d')
1431+
await nextTick()
1432+
expect(container.innerHTML).toBe(
1433+
`<div><!--[--><span>a</span><span>b</span><span>c</span><span>d</span><!--]--></div>`,
1434+
)
1435+
})
1436+
1437+
test('v-for with anchor insertion', async () => {
1438+
const { container, data } = await testHydration(
1439+
`<template>
1440+
<div>
1441+
<span/>
1442+
<span v-for="item in data" :key="item">{{ item }}</span>
1443+
<span/>
1444+
</div>
1445+
</template>`,
1446+
undefined,
1447+
ref(['a', 'b', 'c']),
1448+
)
1449+
expect(container.innerHTML).toBe(
1450+
`<div>` +
1451+
`<span></span>` +
1452+
`<!--[-->` +
1453+
`<span>a</span>` +
1454+
`<span>b</span>` +
1455+
`<span>c</span>` +
1456+
`<!--]-->` +
1457+
`<span></span>` +
1458+
`</div>`,
1459+
)
1460+
1461+
data.value.push('d')
1462+
await nextTick()
1463+
expect(container.innerHTML).toBe(
1464+
`<div>` +
1465+
`<span></span>` +
1466+
`<!--[-->` +
1467+
`<span>a</span>` +
1468+
`<span>b</span>` +
1469+
`<span>c</span>` +
1470+
`<span>d</span>` +
1471+
`<!--]-->` +
1472+
`<span></span>` +
1473+
`</div>`,
1474+
)
1475+
})
1476+
1477+
test('consecutive v-for with anchor insertion', async () => {
1478+
const { container, data } = await testHydration(
1479+
`<template>
1480+
<div>
1481+
<span/>
1482+
<span v-for="item in data" :key="item">{{ item }}</span>
1483+
<span v-for="item in data" :key="item">{{ item }}</span>
1484+
<span/>
1485+
</div>
1486+
</template>`,
1487+
undefined,
1488+
ref(['a', 'b', 'c']),
1489+
)
1490+
expect(container.innerHTML).toBe(
1491+
`<div>` +
1492+
`<span></span>` +
1493+
`<!--[-->` +
1494+
`<span>a</span>` +
1495+
`<span>b</span>` +
1496+
`<span>c</span>` +
1497+
`<!--]-->` +
1498+
`<!--[[-->` +
1499+
`<!--[-->` +
1500+
`<span>a</span>` +
1501+
`<span>b</span>` +
1502+
`<span>c</span>` +
1503+
`<!--]-->` +
1504+
`<!--]]-->` +
1505+
`<span></span>` +
1506+
`</div>`,
1507+
)
1508+
1509+
data.value.push('d')
1510+
await nextTick()
1511+
expect(container.innerHTML).toBe(
1512+
`<div>` +
1513+
`<span></span>` +
1514+
`<!--[-->` +
1515+
`<span>a</span>` +
1516+
`<span>b</span>` +
1517+
`<span>c</span>` +
1518+
`<span>d</span>` +
1519+
`<!--]-->` +
1520+
`<!--[[-->` +
1521+
`<!--[-->` +
1522+
`<span>a</span>` +
1523+
`<span>b</span>` +
1524+
`<span>c</span>` +
1525+
`<span>d</span>` +
1526+
`<!--]-->` +
1527+
`<!--]]-->` +
1528+
`<span></span>` +
1529+
`</div>`,
1530+
)
1531+
})
1532+
1533+
// TODO wait for slots hydration support
1534+
test.todo('v-for on component', async () => {})
1535+
1536+
// TODO wait for slots hydration support
1537+
test.todo('on fragment component', async () => {})
1538+
1539+
// TODO wait for vapor TransitionGroup support
1540+
// v-for inside TransitionGroup does not render as a fragment
1541+
test.todo('v-for in TransitionGroup', async () => {})
1542+
})
13151543

13161544
test.todo('slots')
13171545

packages/runtime-vapor/src/apiCreateFor.ts

+31-7
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@ import {
99
shallowRef,
1010
toReactive,
1111
} from '@vue/reactivity'
12-
import { getSequence, isArray, isObject, isString } from '@vue/shared'
13-
import { createComment, createTextNode } from './dom/node'
12+
import {
13+
FOR_ANCHOR_LABEL,
14+
getSequence,
15+
isArray,
16+
isObject,
17+
isString,
18+
} from '@vue/shared'
19+
import { createComment, createTextNode, nextSiblingAnchor } from './dom/node'
1420
import {
1521
type Block,
1622
VaporFragment,
@@ -22,8 +28,17 @@ import { currentInstance, isVaporComponent } from './component'
2228
import type { DynamicSlot } from './componentSlots'
2329
import { renderEffect } from './renderEffect'
2430
import { VaporVForFlags } from '../../shared/src/vaporFlags'
25-
import { isHydrating, locateHydrationNode } from './dom/hydration'
26-
import { insertionAnchor, insertionParent } from './insertionState'
31+
import {
32+
currentHydrationNode,
33+
isComment,
34+
isHydrating,
35+
locateHydrationNode,
36+
} from './dom/hydration'
37+
import {
38+
insertionAnchor,
39+
insertionParent,
40+
resetInsertionState,
41+
} from './insertionState'
2742

2843
class ForBlock extends VaporFragment {
2944
scope: EffectScope | undefined
@@ -71,15 +86,24 @@ export const createFor = (
7186
const _insertionParent = insertionParent
7287
const _insertionAnchor = insertionAnchor
7388
if (isHydrating) {
74-
locateHydrationNode()
89+
locateHydrationNode(true)
90+
} else {
91+
resetInsertionState()
7592
}
7693

7794
let isMounted = false
7895
let oldBlocks: ForBlock[] = []
7996
let newBlocks: ForBlock[]
8097
let parent: ParentNode | undefined | null
81-
// TODO handle this in hydration
82-
const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
98+
const parentAnchor = isHydrating
99+
? // Use fragment end anchor if available, otherwise use the specific for anchor.
100+
nextSiblingAnchor(
101+
currentHydrationNode!,
102+
isComment(currentHydrationNode!, '[') ? ']' : FOR_ANCHOR_LABEL,
103+
)!
104+
: __DEV__
105+
? createComment('for')
106+
: createTextNode()
83107
const frag = new VaporFragment(oldBlocks)
84108
const instance = currentInstance!
85109
const canUseFastRemove = flags & VaporVForFlags.FAST_REMOVE

packages/runtime-vapor/src/dom/hydration.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
disableHydrationNodeLookup,
1111
enableHydrationNodeLookup,
1212
next,
13-
prev,
1413
} from './node'
1514
import { isDynamicFragmentEndAnchor } from '@vue/shared'
1615

@@ -98,7 +97,7 @@ function locateHydrationNodeImpl(isFragment?: boolean) {
9897
// if the last child is a comment, it is the anchor for the fragment
9998
// so it need to find the previous node
10099
if (isFragment && node && isDynamicFragmentEndAnchor(node)) {
101-
let previous = prev(node)
100+
let previous = node.previousSibling //prev(node)
102101
if (previous) node = previous
103102
}
104103

0 commit comments

Comments
 (0)