Skip to content

Commit 4141370

Browse files
committed
fix(hydration): handle v-if on insertion parent
1 parent cb7779b commit 4141370

File tree

4 files changed

+80
-1
lines changed

4 files changed

+80
-1
lines changed

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,6 +1134,28 @@ describe('Vapor Mode hydration', () => {
11341134
expect(container.innerHTML).toBe(`<div>foo</div><!---->`)
11351135
})
11361136

1137+
test('v-if on insertion parent', async () => {
1138+
const data = ref(true)
1139+
const { container } = await testHydration(
1140+
`<template>
1141+
<div v-if="data">
1142+
<components.Child/>
1143+
</div>
1144+
</template>`,
1145+
{ Child: `<template>foo</template>` },
1146+
data,
1147+
)
1148+
expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
1149+
1150+
data.value = false
1151+
await nextTick()
1152+
expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
1153+
1154+
data.value = true
1155+
await nextTick()
1156+
expect(container.innerHTML).toBe(`<div>foo</div><!--${anchorLabel}-->`)
1157+
})
1158+
11371159
test('v-if/else-if/else chain - switch branches', async () => {
11381160
const data = ref('a')
11391161
const { container } = await testHydration(

packages/runtime-vapor/src/apiCreateIf.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,40 @@ import {
88
} from './insertionState'
99
import { renderEffect } from './renderEffect'
1010

11+
const ifStack = [] as DynamicFragment[]
12+
const insertionParents = new WeakMap<DynamicFragment, Node[]>()
13+
14+
/**
15+
* Collects insertionParents inside an if block during hydration
16+
* When the if condition becomes false on the client, clears the
17+
* HTML of these insertionParents to prevent duplicate rendering
18+
* results when the condition becomes true again
19+
*
20+
* Example:
21+
* const t2 = _template("<div></div>")
22+
* const n2 = _createIf(() => show.value, () => {
23+
* const n5 = t2()
24+
* _setInsertionState(n5)
25+
* const n4 = _createComponent(Comp) // renders `<span></span>`
26+
* return n5
27+
* })
28+
*
29+
* After hydration, the HTML of `n5` is `<div><span></span></div>` instead of `<div></div>`.
30+
* When `show.value` becomes false, the HTML of `n5` needs to be cleared,
31+
* to avoid duplicated rendering when `show.value` becomes true again.
32+
*/
33+
export function collectInsertionParents(insertionParent: ParentNode): void {
34+
const currentIf = ifStack[ifStack.length - 1]
35+
if (currentIf) {
36+
let nodes = insertionParents.get(currentIf)
37+
if (!nodes) {
38+
nodes = []
39+
insertionParents.set(currentIf, nodes)
40+
}
41+
nodes.push(insertionParent)
42+
}
43+
}
44+
1145
export function createIf(
1246
condition: () => any,
1347
b1: BlockFn,
@@ -26,7 +60,19 @@ export function createIf(
2660
isHydrating || __DEV__
2761
? new DynamicFragment(IF_ANCHOR_LABEL)
2862
: new DynamicFragment()
63+
if (isHydrating) {
64+
;(frag as DynamicFragment).teardown = () => {
65+
const nodes = insertionParents.get(frag as DynamicFragment)
66+
if (nodes) {
67+
nodes.forEach(p => ((p as Element).innerHTML = ''))
68+
insertionParents.delete(frag as DynamicFragment)
69+
}
70+
;(frag as DynamicFragment).teardown = undefined
71+
}
72+
ifStack.push(frag as DynamicFragment)
73+
}
2974
renderEffect(() => (frag as DynamicFragment).update(condition() ? b1 : b2))
75+
isHydrating && ifStack.pop()
3076
}
3177

3278
if (!isHydrating && _insertionParent) {

packages/runtime-vapor/src/block.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export class DynamicFragment extends VaporFragment {
4040
scope: EffectScope | undefined
4141
current?: BlockFn
4242
fallback?: BlockFn
43+
teardown?: () => void
4344

4445
constructor(anchorLabel?: string) {
4546
super([])
@@ -64,7 +65,10 @@ export class DynamicFragment extends VaporFragment {
6465
// teardown previous branch
6566
if (this.scope) {
6667
this.scope.stop()
67-
parent && remove(this.nodes, parent)
68+
if (parent) {
69+
remove(this.nodes, parent)
70+
this.teardown && this.teardown()
71+
}
6872
}
6973

7074
if (render) {

packages/runtime-vapor/src/insertionState.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { collectInsertionParents } from './apiCreateIf'
2+
import { isHydrating } from './dom/hydration'
3+
14
export let insertionParent:
25
| (ParentNode & {
36
// dynamic node position - hydration only
@@ -21,6 +24,10 @@ export let insertionAnchor: Node | 0 | undefined
2124
export function setInsertionState(parent: ParentNode, anchor?: Node | 0): void {
2225
insertionParent = parent
2326
insertionAnchor = anchor
27+
28+
if (isHydrating) {
29+
collectInsertionParents(parent)
30+
}
2431
}
2532

2633
export function resetInsertionState(): void {

0 commit comments

Comments
 (0)