Skip to content

Commit 595263c

Browse files
committed
fix(ssr/teleport): support nested teleports in ssr
fix #5242
1 parent 84f0353 commit 595263c

File tree

4 files changed

+86
-29
lines changed

4 files changed

+86
-29
lines changed

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

+38-6
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ describe('SSR hydration', () => {
202202
const fn = jest.fn()
203203
const teleportContainer = document.createElement('div')
204204
teleportContainer.id = 'teleport'
205-
teleportContainer.innerHTML = `<span>foo</span><span class="foo"></span><!---->`
205+
teleportContainer.innerHTML = `<span>foo</span><span class="foo"></span><!--teleport anchor-->`
206206
document.body.appendChild(teleportContainer)
207207

208208
const { vnode, container } = mountWithHydration(
@@ -233,7 +233,7 @@ describe('SSR hydration', () => {
233233
msg.value = 'bar'
234234
await nextTick()
235235
expect(teleportContainer.innerHTML).toBe(
236-
`<span>bar</span><span class="bar"></span><!---->`
236+
`<span>bar</span><span class="bar"></span><!--teleport anchor-->`
237237
)
238238
})
239239

@@ -263,7 +263,7 @@ describe('SSR hydration', () => {
263263

264264
const teleportHtml = ctx.teleports!['#teleport2']
265265
expect(teleportHtml).toMatchInlineSnapshot(
266-
`"<span>foo</span><span class=\\"foo\\"></span><!----><span>foo2</span><span class=\\"foo2\\"></span><!---->"`
266+
`"<span>foo</span><span class=\\"foo\\"></span><!--teleport anchor--><span>foo2</span><span class=\\"foo2\\"></span><!--teleport anchor-->"`
267267
)
268268

269269
teleportContainer.innerHTML = teleportHtml
@@ -300,7 +300,7 @@ describe('SSR hydration', () => {
300300
msg.value = 'bar'
301301
await nextTick()
302302
expect(teleportContainer.innerHTML).toMatchInlineSnapshot(
303-
`"<span>bar</span><span class=\\"bar\\"></span><!----><span>bar2</span><span class=\\"bar2\\"></span><!---->"`
303+
`"<span>bar</span><span class=\\"bar\\"></span><!--teleport anchor--><span>bar2</span><span class=\\"bar2\\"></span><!--teleport anchor-->"`
304304
)
305305
})
306306

@@ -327,7 +327,7 @@ describe('SSR hydration', () => {
327327
)
328328

329329
const teleportHtml = ctx.teleports!['#teleport3']
330-
expect(teleportHtml).toMatchInlineSnapshot(`"<!---->"`)
330+
expect(teleportHtml).toMatchInlineSnapshot(`"<!--teleport anchor-->"`)
331331

332332
teleportContainer.innerHTML = teleportHtml
333333
document.body.appendChild(teleportContainer)
@@ -369,7 +369,7 @@ describe('SSR hydration', () => {
369369
test('Teleport (as component root)', () => {
370370
const teleportContainer = document.createElement('div')
371371
teleportContainer.id = 'teleport4'
372-
teleportContainer.innerHTML = `hello<!---->`
372+
teleportContainer.innerHTML = `hello<!--teleport anchor-->`
373373
document.body.appendChild(teleportContainer)
374374

375375
const wrapper = {
@@ -395,6 +395,38 @@ describe('SSR hydration', () => {
395395
expect(nextVNode.el).toBe(container.firstChild?.lastChild)
396396
})
397397

398+
test('Teleport (nested)', () => {
399+
const teleportContainer = document.createElement('div')
400+
teleportContainer.id = 'teleport5'
401+
teleportContainer.innerHTML = `<div><!--teleport start--><!--teleport end--></div><!--teleport anchor--><div>child</div><!--teleport anchor-->`
402+
document.body.appendChild(teleportContainer)
403+
404+
const { vnode, container } = mountWithHydration(
405+
'<!--teleport start--><!--teleport end-->',
406+
() =>
407+
h(Teleport, { to: '#teleport5' }, [
408+
h('div', [h(Teleport, { to: '#teleport5' }, [h('div', 'child')])])
409+
])
410+
)
411+
412+
expect(vnode.el).toBe(container.firstChild)
413+
expect(vnode.anchor).toBe(container.lastChild)
414+
415+
const childDivVNode = (vnode as any).children[0]
416+
const div = teleportContainer.firstChild
417+
expect(childDivVNode.el).toBe(div)
418+
expect(vnode.targetAnchor).toBe(div?.nextSibling)
419+
420+
const childTeleportVNode = childDivVNode.children[0]
421+
expect(childTeleportVNode.el).toBe(div?.firstChild)
422+
expect(childTeleportVNode.anchor).toBe(div?.lastChild)
423+
424+
expect(childTeleportVNode.targetAnchor).toBe(teleportContainer.lastChild)
425+
expect(childTeleportVNode.children[0].el).toBe(
426+
teleportContainer.lastChild?.previousSibling
427+
)
428+
})
429+
398430
// compile SSR + client render fn from the same template & hydrate
399431
test('full compiler integration', async () => {
400432
const mounted: string[] = []

packages/runtime-core/src/components/Teleport.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,26 @@ function hydrateTeleport(
353353
vnode.targetAnchor = targetNode
354354
} else {
355355
vnode.anchor = nextSibling(node)
356-
vnode.targetAnchor = hydrateChildren(
356+
357+
// lookahead until we find the target anchor
358+
// we cannot rely on return value of hydrateChildren() because there
359+
// could be nested teleports
360+
let targetAnchor = targetNode
361+
while (targetAnchor) {
362+
targetAnchor = nextSibling(targetAnchor)
363+
if (
364+
targetAnchor &&
365+
targetAnchor.nodeType === 8 &&
366+
(targetAnchor as Comment).data === 'teleport anchor'
367+
) {
368+
vnode.targetAnchor = targetAnchor
369+
;(target as TeleportTargetElement)._lpa =
370+
vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
371+
break
372+
}
373+
}
374+
375+
hydrateChildren(
357376
targetNode,
358377
vnode,
359378
target,
@@ -363,8 +382,6 @@ function hydrateTeleport(
363382
optimized
364383
)
365384
}
366-
;(target as TeleportTargetElement)._lpa =
367-
vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
368385
}
369386
}
370387
return vnode.anchor && nextSibling(vnode.anchor as Node)

packages/server-renderer/__tests__/ssrTeleport.spec.ts

+15-7
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ describe('ssrRenderTeleport', () => {
3131
ctx
3232
)
3333
expect(html).toBe('<!--teleport start--><!--teleport end-->')
34-
expect(ctx.teleports!['#target']).toBe(`<div>content</div><!---->`)
34+
expect(ctx.teleports!['#target']).toBe(
35+
`<div>content</div><!--teleport anchor-->`
36+
)
3537
})
3638

3739
test('teleport rendering (compiled + disabled)', async () => {
@@ -58,7 +60,7 @@ describe('ssrRenderTeleport', () => {
5860
expect(html).toBe(
5961
'<!--teleport start--><div>content</div><!--teleport end-->'
6062
)
61-
expect(ctx.teleports!['#target']).toBe(`<!---->`)
63+
expect(ctx.teleports!['#target']).toBe(`<!--teleport anchor-->`)
6264
})
6365

6466
test('teleport rendering (vnode)', async () => {
@@ -74,7 +76,9 @@ describe('ssrRenderTeleport', () => {
7476
ctx
7577
)
7678
expect(html).toBe('<!--teleport start--><!--teleport end-->')
77-
expect(ctx.teleports!['#target']).toBe('<span>hello</span><!---->')
79+
expect(ctx.teleports!['#target']).toBe(
80+
'<span>hello</span><!--teleport anchor-->'
81+
)
7882
})
7983

8084
test('teleport rendering (vnode + disabled)', async () => {
@@ -93,7 +97,7 @@ describe('ssrRenderTeleport', () => {
9397
expect(html).toBe(
9498
'<!--teleport start--><span>hello</span><!--teleport end-->'
9599
)
96-
expect(ctx.teleports!['#target']).toBe(`<!---->`)
100+
expect(ctx.teleports!['#target']).toBe(`<!--teleport anchor-->`)
97101
})
98102

99103
test('multiple teleports with same target', async () => {
@@ -115,7 +119,7 @@ describe('ssrRenderTeleport', () => {
115119
'<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>'
116120
)
117121
expect(ctx.teleports!['#target']).toBe(
118-
'<span>hello</span><!---->world<!---->'
122+
'<span>hello</span><!--teleport anchor-->world<!--teleport anchor-->'
119123
)
120124
})
121125

@@ -133,7 +137,9 @@ describe('ssrRenderTeleport', () => {
133137
ctx
134138
)
135139
expect(html).toBe('<!--teleport start--><!--teleport end-->')
136-
expect(ctx.teleports!['#target']).toBe(`<div>content</div><!---->`)
140+
expect(ctx.teleports!['#target']).toBe(
141+
`<div>content</div><!--teleport anchor-->`
142+
)
137143
})
138144

139145
test('teleport inside async component (stream)', async () => {
@@ -166,6 +172,8 @@ describe('ssrRenderTeleport', () => {
166172
)
167173
await p
168174
expect(html).toBe('<!--teleport start--><!--teleport end-->')
169-
expect(ctx.teleports!['#target']).toBe(`<div>content</div><!---->`)
175+
expect(ctx.teleports!['#target']).toBe(
176+
`<div>content</div><!--teleport anchor-->`
177+
)
170178
})
171179
})

packages/server-renderer/src/helpers/ssrRenderTeleport.ts

+13-13
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,28 @@ export function ssrRenderTeleport(
1010
) {
1111
parentPush('<!--teleport start-->')
1212

13+
const context = parentComponent.appContext.provides[
14+
ssrContextKey as any
15+
] as SSRContext
16+
const teleportBuffers =
17+
context.__teleportBuffers || (context.__teleportBuffers = {})
18+
const targetBuffer = teleportBuffers[target] || (teleportBuffers[target] = [])
19+
// record current index of the target buffer to handle nested teleports
20+
// since the parent needs to be rendered before the child
21+
const bufferIndex = targetBuffer.length
22+
1323
let teleportContent: SSRBufferItem
1424

1525
if (disabled) {
1626
contentRenderFn(parentPush)
17-
teleportContent = `<!---->`
27+
teleportContent = `<!--teleport anchor-->`
1828
} else {
1929
const { getBuffer, push } = createBuffer()
2030
contentRenderFn(push)
21-
push(`<!---->`) // teleport end anchor
31+
push(`<!--teleport anchor-->`)
2232
teleportContent = getBuffer()
2333
}
2434

25-
const context = parentComponent.appContext.provides[
26-
ssrContextKey as any
27-
] as SSRContext
28-
const teleportBuffers =
29-
context.__teleportBuffers || (context.__teleportBuffers = {})
30-
if (teleportBuffers[target]) {
31-
teleportBuffers[target].push(teleportContent)
32-
} else {
33-
teleportBuffers[target] = [teleportContent]
34-
}
35-
35+
targetBuffer.splice(bufferIndex, 0, teleportContent)
3636
parentPush('<!--teleport end-->')
3737
}

0 commit comments

Comments
 (0)