Skip to content

Commit b1a0c00

Browse files
committed
wip: hydration
1 parent bba71be commit b1a0c00

File tree

13 files changed

+332
-117
lines changed

13 files changed

+332
-117
lines changed

packages/compiler-vapor/src/generators/template.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ export function genSelf(
2424
context: CodegenContext,
2525
): CodeFragment[] {
2626
const [frag, push] = buildCodeFragment()
27-
const { id, template, operation } = dynamic
27+
const { id, template, operation, dynamicChildOffset } = dynamic
2828

2929
if (id !== undefined && template !== undefined) {
30-
push(NEWLINE, `const n${id} = t${template}()`)
30+
push(NEWLINE, `const n${id} = t${template}(${dynamicChildOffset || ''})`)
3131
push(...genDirectivesForElement(id, context))
3232
}
3333

packages/compiler-vapor/src/ir/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ export interface IRDynamicInfo {
266266
children: IRDynamicInfo[]
267267
template?: number
268268
hasDynamicChild?: boolean
269+
dynamicChildOffset?: number
269270
operation?: OperationNode
270271
needsKey?: boolean
271272
}

packages/compiler-vapor/src/transforms/transformChildren.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const transformChildren: NodeTransform = (node, context) => {
5959

6060
function processDynamicChildren(context: TransformContext<ElementNode>) {
6161
let prevDynamics: IRDynamicInfo[] = []
62-
let hasStaticTemplate = false
62+
let staticCount = 0
6363
const children = context.dynamic.children
6464

6565
for (const [index, child] of children.entries()) {
@@ -69,7 +69,7 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
6969

7070
if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
7171
if (prevDynamics.length) {
72-
if (hasStaticTemplate) {
72+
if (staticCount) {
7373
// each dynamic child gets its own placeholder node.
7474
// this makes it easier to locate the corresponding node during hydration.
7575
for (let i = 0; i < prevDynamics.length; i++) {
@@ -92,12 +92,13 @@ function processDynamicChildren(context: TransformContext<ElementNode>) {
9292
}
9393
prevDynamics = []
9494
}
95-
hasStaticTemplate = true
95+
staticCount++
9696
}
9797
}
9898

9999
if (prevDynamics.length) {
100-
registerInsertion(prevDynamics, context)
100+
registerInsertion(prevDynamics, context, undefined)
101+
context.dynamic.dynamicChildOffset = staticCount
101102
}
102103
}
103104

packages/runtime-core/src/apiCreateApp.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ export interface VaporInteropInterface {
191191
transition: TransitionHooks,
192192
): void
193193
hydrate(node: Node, fn: () => void): void
194+
hydrateSlot(vnode: VNode, container: any): void
194195

195196
vdomMount: (
196197
component: ConcreteComponent,

packages/runtime-core/src/hydration.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
Comment as VComment,
66
type VNode,
77
type VNodeHook,
8+
VaporSlot,
89
createTextVNode,
910
createVNode,
1011
invokeVNodeHook,
@@ -276,6 +277,12 @@ export function createHydrationFunctions(
276277
)
277278
}
278279
break
280+
case VaporSlot:
281+
getVaporInterface(parentComponent, vnode).hydrateSlot(
282+
vnode,
283+
parentNode(node)!,
284+
)
285+
break
279286
default:
280287
if (shapeFlag & ShapeFlags.ELEMENT) {
281288
if (

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

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,34 @@ describe('Vapor Mode hydration', () => {
476476
)
477477
})
478478

479+
test('consecutive components with insertion parent', async () => {
480+
const data = reactive({ foo: 'foo', bar: 'bar' })
481+
const { container } = await testHydration(
482+
`<template>
483+
<div>
484+
<components.Child1/>
485+
<components.Child2/>
486+
</div>
487+
</template>
488+
`,
489+
{
490+
Child1: `<template><span>{{ data.foo }}</span></template>`,
491+
Child2: `<template><span>{{ data.bar }}</span></template>`,
492+
},
493+
data,
494+
)
495+
expect(container.innerHTML).toBe(
496+
`<div><span>foo</span><span>bar</span></div>`,
497+
)
498+
499+
data.foo = 'foo1'
500+
data.bar = 'bar1'
501+
await nextTick()
502+
expect(container.innerHTML).toBe(
503+
`<div><span>foo1</span><span>bar1</span></div>`,
504+
)
505+
})
506+
479507
test('nested consecutive components with anchor insertion', async () => {
480508
const { container, data } = await testHydration(
481509
`
@@ -1046,6 +1074,27 @@ describe('Vapor Mode hydration', () => {
10461074
`</div>`,
10471075
)
10481076
})
1077+
1078+
test('dynamic component fallback', async () => {
1079+
const { container, data } = await testHydration(
1080+
`<template>
1081+
<component :is="'button'">
1082+
<span>{{ data }}</span>
1083+
</component>
1084+
</template>`,
1085+
{},
1086+
ref('foo'),
1087+
)
1088+
1089+
expect(container.innerHTML).toBe(
1090+
`<button><span>foo</span></button><!--${anchorLabel}-->`,
1091+
)
1092+
data.value = 'bar'
1093+
await nextTick()
1094+
expect(container.innerHTML).toBe(
1095+
`<button><span>bar</span></button><!--${anchorLabel}-->`,
1096+
)
1097+
})
10491098
})
10501099

10511100
describe('if', () => {
@@ -1314,6 +1363,38 @@ describe('Vapor Mode hydration', () => {
13141363
)
13151364
})
13161365

1366+
test('consecutive component with insertion parent', async () => {
1367+
const data = reactive({
1368+
show: true,
1369+
foo: 'foo',
1370+
bar: 'bar',
1371+
})
1372+
const { container } = await testHydration(
1373+
`<template>
1374+
<div v-if="data.show">
1375+
<components.Child/>
1376+
<components.Child2/>
1377+
</div>
1378+
</template>`,
1379+
{
1380+
Child: `<template><span>{{data.foo}}</span></template>`,
1381+
Child2: `<template><span>{{data.bar}}</span></template>`,
1382+
},
1383+
data,
1384+
)
1385+
expect(container.innerHTML).toBe(
1386+
`<div>` +
1387+
`<span>foo</span>` +
1388+
`<span>bar</span>` +
1389+
`</div>` +
1390+
`<!--${anchorLabel}-->`,
1391+
)
1392+
1393+
data.show = false
1394+
await nextTick()
1395+
expect(container.innerHTML).toBe(`<!--${anchorLabel}-->`)
1396+
})
1397+
13171398
test('consecutive v-if on component with anchor insertion', async () => {
13181399
const data = ref(true)
13191400
const { container } = await testHydration(
@@ -2354,6 +2435,31 @@ describe('Vapor Mode hydration', () => {
23542435
`</div>`,
23552436
)
23562437
})
2438+
2439+
test('slot fallback', async () => {
2440+
const data = reactive({
2441+
foo: 'foo',
2442+
})
2443+
const { container } = await testHydration(
2444+
`<template>
2445+
<components.Child>
2446+
</components.Child>
2447+
</template>`,
2448+
{
2449+
Child: `<template><slot><span>{{data.foo}}</span></slot></template>`,
2450+
},
2451+
data,
2452+
)
2453+
expect(container.innerHTML).toBe(
2454+
`<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->`,
2455+
)
2456+
2457+
data.foo = 'bar'
2458+
await nextTick()
2459+
expect(container.innerHTML).toBe(
2460+
`<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->`,
2461+
)
2462+
})
23572463
})
23582464

23592465
describe.todo('transition', async () => {
@@ -3912,6 +4018,76 @@ describe('VDOM hydration interop', () => {
39124018
expect(container.innerHTML).toMatchInlineSnapshot(`"false"`)
39134019
})
39144020

4021+
test('nested components (VDOM -> Vapor -> VDOM (with slot fallback))', async () => {
4022+
const data = ref(true)
4023+
const { container } = await testHydrationInterop(
4024+
`<script setup>const data = _data; const components = _components;</script>
4025+
<template>
4026+
<components.VaporChild/>
4027+
</template>`,
4028+
{
4029+
VaporChild: {
4030+
code: `<template><components.VdomChild/></template>`,
4031+
vapor: true,
4032+
},
4033+
VdomChild: {
4034+
code: `<script setup>const data = _data;</script>
4035+
<template><slot><span>{{data}}</span></slot></template>`,
4036+
vapor: false,
4037+
},
4038+
},
4039+
data,
4040+
)
4041+
4042+
expect(container.innerHTML).toMatchInlineSnapshot(
4043+
`"<!--[--><span>true</span><!--]--><!--slot-->"`,
4044+
)
4045+
4046+
data.value = false
4047+
await nextTick()
4048+
expect(container.innerHTML).toMatchInlineSnapshot(
4049+
`"<!--[--><span>false</span><!--]--><!--slot-->"`,
4050+
)
4051+
})
4052+
4053+
test('nested components (VDOM -> Vapor(with slot fallback) -> VDOM)', async () => {
4054+
const data = ref(true)
4055+
const { container } = await testHydrationInterop(
4056+
`<script setup>const data = _data; const components = _components;</script>
4057+
<template>
4058+
<components.VaporChild/>
4059+
</template>`,
4060+
{
4061+
VaporChild: {
4062+
code: `<template>
4063+
<components.VdomChild>
4064+
<template #default>
4065+
<span>{{data}} vapor fallback</span>
4066+
</template>
4067+
</components.VdomChild>
4068+
</template>`,
4069+
vapor: true,
4070+
},
4071+
VdomChild: {
4072+
code: `<script setup>const data = _data;</script>
4073+
<template><slot><span>vdom fallback</span></slot></template>`,
4074+
vapor: false,
4075+
},
4076+
},
4077+
data,
4078+
)
4079+
4080+
expect(container.innerHTML).toMatchInlineSnapshot(
4081+
`"<!--[--><span>true vapor fallback</span><!--]--><!--slot-->"`,
4082+
)
4083+
4084+
data.value = false
4085+
await nextTick()
4086+
expect(container.innerHTML).toMatchInlineSnapshot(
4087+
`"<!--[--><span>false vapor fallback</span><!--]--><!--slot-->"`,
4088+
)
4089+
})
4090+
39154091
test('vapor slot render vdom component', async () => {
39164092
const data = ref(true)
39174093
const { container } = await testHydrationInterop(

packages/runtime-vapor/src/apiCreateFor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export const createFor = (
8686
const _insertionParent = insertionParent
8787
const _insertionAnchor = insertionAnchor
8888
if (isHydrating) {
89-
locateHydrationNode(true)
89+
locateHydrationNode()
9090
} else {
9191
resetInsertionState()
9292
}

packages/runtime-vapor/src/component.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,13 @@ import {
6666
} from './componentSlots'
6767
import { hmrReload, hmrRerender } from './hmr'
6868
import { createElement } from './dom/node'
69-
import { isHydrating, locateHydrationNode } from './dom/hydration'
69+
import {
70+
adoptTemplate,
71+
currentHydrationNode,
72+
isHydrating,
73+
locateHydrationNode,
74+
setCurrentHydrationNode,
75+
} from './dom/hydration'
7076
import { isVaporTeleport } from './components/Teleport'
7177
import {
7278
insertionAnchor,
@@ -533,7 +539,9 @@ export function createComponentWithFallback(
533539
resetInsertionState()
534540
}
535541

536-
const el = createElement(comp)
542+
const el = isHydrating
543+
? (adoptTemplate(currentHydrationNode!, `<${comp}/>`) as HTMLElement)
544+
: createElement(comp)
537545
// mark single root
538546
;(el as any).$root = isSingleRoot
539547

@@ -547,6 +555,7 @@ export function createComponentWithFallback(
547555
}
548556

549557
if (rawSlots) {
558+
isHydrating && setCurrentHydrationNode(el.firstChild)
550559
if (rawSlots.$) {
551560
// TODO dynamic slot fragment
552561
} else {

0 commit comments

Comments
 (0)