diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts
index 76e8b153..58661233 100644
--- a/packages/runtime-core/__tests__/hydration.spec.ts
+++ b/packages/runtime-core/__tests__/hydration.spec.ts
@@ -202,7 +202,7 @@ describe('SSR hydration', () => {
const fn = jest.fn()
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport'
- teleportContainer.innerHTML = `foo`
+ teleportContainer.innerHTML = `foo`
document.body.appendChild(teleportContainer)
const { vnode, container } = mountWithHydration(
@@ -233,7 +233,7 @@ describe('SSR hydration', () => {
msg.value = 'bar'
await nextTick()
expect(teleportContainer.innerHTML).toBe(
- `bar`
+ `bar`
)
})
@@ -263,7 +263,7 @@ describe('SSR hydration', () => {
const teleportHtml = ctx.teleports!['#teleport2']
expect(teleportHtml).toMatchInlineSnapshot(
- `"foofoo2"`
+ `"foofoo2"`
)
teleportContainer.innerHTML = teleportHtml
@@ -300,7 +300,7 @@ describe('SSR hydration', () => {
msg.value = 'bar'
await nextTick()
expect(teleportContainer.innerHTML).toMatchInlineSnapshot(
- `"barbar2"`
+ `"barbar2"`
)
})
@@ -327,7 +327,7 @@ describe('SSR hydration', () => {
)
const teleportHtml = ctx.teleports!['#teleport3']
- expect(teleportHtml).toMatchInlineSnapshot(`""`)
+ expect(teleportHtml).toMatchInlineSnapshot(`""`)
teleportContainer.innerHTML = teleportHtml
document.body.appendChild(teleportContainer)
@@ -369,7 +369,7 @@ describe('SSR hydration', () => {
test('Teleport (as component root)', () => {
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport4'
- teleportContainer.innerHTML = `hello`
+ teleportContainer.innerHTML = `hello`
document.body.appendChild(teleportContainer)
const wrapper = {
@@ -395,6 +395,38 @@ describe('SSR hydration', () => {
expect(nextVNode.el).toBe(container.firstChild?.lastChild)
})
+ test('Teleport (nested)', () => {
+ const teleportContainer = document.createElement('div')
+ teleportContainer.id = 'teleport5'
+ teleportContainer.innerHTML = `
child
`
+ document.body.appendChild(teleportContainer)
+
+ const { vnode, container } = mountWithHydration(
+ '',
+ () =>
+ h(Teleport, { to: '#teleport5' }, [
+ h('div', [h(Teleport, { to: '#teleport5' }, [h('div', 'child')])])
+ ])
+ )
+
+ expect(vnode.el).toBe(container.firstChild)
+ expect(vnode.anchor).toBe(container.lastChild)
+
+ const childDivVNode = (vnode as any).children[0]
+ const div = teleportContainer.firstChild
+ expect(childDivVNode.el).toBe(div)
+ expect(vnode.targetAnchor).toBe(div?.nextSibling)
+
+ const childTeleportVNode = childDivVNode.children[0]
+ expect(childTeleportVNode.el).toBe(div?.firstChild)
+ expect(childTeleportVNode.anchor).toBe(div?.lastChild)
+
+ expect(childTeleportVNode.targetAnchor).toBe(teleportContainer.lastChild)
+ expect(childTeleportVNode.children[0].el).toBe(
+ teleportContainer.lastChild?.previousSibling
+ )
+ })
+
// compile SSR + client render fn from the same template & hydrate
test('full compiler integration', async () => {
const mounted: string[] = []
diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts
index 68d50a63..06b69aff 100644
--- a/packages/runtime-core/src/components/Teleport.ts
+++ b/packages/runtime-core/src/components/Teleport.ts
@@ -353,7 +353,26 @@ function hydrateTeleport(
vnode.targetAnchor = targetNode
} else {
vnode.anchor = nextSibling(node)
- vnode.targetAnchor = hydrateChildren(
+
+ // lookahead until we find the target anchor
+ // we cannot rely on return value of hydrateChildren() because there
+ // could be nested teleports
+ let targetAnchor = targetNode
+ while (targetAnchor) {
+ targetAnchor = nextSibling(targetAnchor)
+ if (
+ targetAnchor &&
+ targetAnchor.nodeType === 8 &&
+ (targetAnchor as Comment).data === 'teleport anchor'
+ ) {
+ vnode.targetAnchor = targetAnchor
+ ;(target as TeleportTargetElement)._lpa =
+ vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
+ break
+ }
+ }
+
+ hydrateChildren(
targetNode,
vnode,
target,
@@ -363,8 +382,6 @@ function hydrateTeleport(
optimized
)
}
- ;(target as TeleportTargetElement)._lpa =
- vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
}
}
return vnode.anchor && nextSibling(vnode.anchor as Node)
diff --git a/packages/server-renderer/__tests__/ssrTeleport.spec.ts b/packages/server-renderer/__tests__/ssrTeleport.spec.ts
index af915168..76c5c8eb 100644
--- a/packages/server-renderer/__tests__/ssrTeleport.spec.ts
+++ b/packages/server-renderer/__tests__/ssrTeleport.spec.ts
@@ -31,7 +31,9 @@ describe('ssrRenderTeleport', () => {
ctx
)
expect(html).toBe('')
- expect(ctx.teleports!['#target']).toBe(`content
`)
+ expect(ctx.teleports!['#target']).toBe(
+ `content
`
+ )
})
test('teleport rendering (compiled + disabled)', async () => {
@@ -58,7 +60,7 @@ describe('ssrRenderTeleport', () => {
expect(html).toBe(
'content
'
)
- expect(ctx.teleports!['#target']).toBe(``)
+ expect(ctx.teleports!['#target']).toBe(``)
})
test('teleport rendering (vnode)', async () => {
@@ -74,7 +76,9 @@ describe('ssrRenderTeleport', () => {
ctx
)
expect(html).toBe('')
- expect(ctx.teleports!['#target']).toBe('hello')
+ expect(ctx.teleports!['#target']).toBe(
+ 'hello'
+ )
})
test('teleport rendering (vnode + disabled)', async () => {
@@ -93,7 +97,7 @@ describe('ssrRenderTeleport', () => {
expect(html).toBe(
'hello'
)
- expect(ctx.teleports!['#target']).toBe(``)
+ expect(ctx.teleports!['#target']).toBe(``)
})
test('multiple teleports with same target', async () => {
@@ -115,7 +119,7 @@ describe('ssrRenderTeleport', () => {
''
)
expect(ctx.teleports!['#target']).toBe(
- 'helloworld'
+ 'helloworld'
)
})
@@ -133,7 +137,9 @@ describe('ssrRenderTeleport', () => {
ctx
)
expect(html).toBe('')
- expect(ctx.teleports!['#target']).toBe(`content
`)
+ expect(ctx.teleports!['#target']).toBe(
+ `content
`
+ )
})
test('teleport inside async component (stream)', async () => {
@@ -166,6 +172,8 @@ describe('ssrRenderTeleport', () => {
)
await p
expect(html).toBe('')
- expect(ctx.teleports!['#target']).toBe(`content
`)
+ expect(ctx.teleports!['#target']).toBe(
+ `content
`
+ )
})
})
diff --git a/packages/server-renderer/src/helpers/ssrRenderTeleport.ts b/packages/server-renderer/src/helpers/ssrRenderTeleport.ts
index 77331b7b..8338ec06 100644
--- a/packages/server-renderer/src/helpers/ssrRenderTeleport.ts
+++ b/packages/server-renderer/src/helpers/ssrRenderTeleport.ts
@@ -10,28 +10,28 @@ export function ssrRenderTeleport(
) {
parentPush('')
- let teleportContent: SSRBufferItem
-
- if (disabled) {
- contentRenderFn(parentPush)
- teleportContent = ``
- } else {
- const { getBuffer, push } = createBuffer()
- contentRenderFn(push)
- push(``) // teleport end anchor
- teleportContent = getBuffer()
- }
-
const context = parentComponent.appContext.provides[
ssrContextKey as any
] as SSRContext
const teleportBuffers =
context.__teleportBuffers || (context.__teleportBuffers = {})
- if (teleportBuffers[target]) {
- teleportBuffers[target].push(teleportContent)
+ const targetBuffer = teleportBuffers[target] || (teleportBuffers[target] = [])
+ // record current index of the target buffer to handle nested teleports
+ // since the parent needs to be rendered before the child
+ const bufferIndex = targetBuffer.length
+
+ let teleportContent: SSRBufferItem
+
+ if (disabled) {
+ contentRenderFn(parentPush)
+ teleportContent = ``
} else {
- teleportBuffers[target] = [teleportContent]
+ const { getBuffer, push } = createBuffer()
+ contentRenderFn(push)
+ push(``)
+ teleportContent = getBuffer()
}
+ targetBuffer.splice(bufferIndex, 0, teleportContent)
parentPush('')
}