diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts
index 9916fafa62c..c525618b510 100644
--- a/packages/runtime-core/__tests__/hydration.spec.ts
+++ b/packages/runtime-core/__tests__/hydration.spec.ts
@@ -265,7 +265,7 @@ describe('SSR hydration', () => {
const fn = vi.fn()
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport'
- teleportContainer.innerHTML = `foo`
+ teleportContainer.innerHTML = `foo`
document.body.appendChild(teleportContainer)
const { vnode, container } = mountWithHydration(
@@ -281,13 +281,14 @@ describe('SSR hydration', () => {
expect(vnode.anchor).toBe(container.lastChild)
expect(vnode.target).toBe(teleportContainer)
+ expect(vnode.targetStart).toBe(teleportContainer.childNodes[0])
expect((vnode.children as VNode[])[0].el).toBe(
- teleportContainer.childNodes[0],
+ teleportContainer.childNodes[1],
)
expect((vnode.children as VNode[])[1].el).toBe(
- teleportContainer.childNodes[1],
+ teleportContainer.childNodes[2],
)
- expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[2])
+ expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[3])
// event handler
triggerEvent('click', teleportContainer.querySelector('.foo')!)
@@ -296,7 +297,7 @@ describe('SSR hydration', () => {
msg.value = 'bar'
await nextTick()
expect(teleportContainer.innerHTML).toBe(
- `bar`,
+ `bar`,
)
})
@@ -326,7 +327,7 @@ describe('SSR hydration', () => {
const teleportHtml = ctx.teleports!['#teleport2']
expect(teleportHtml).toMatchInlineSnapshot(
- `"foofoo2"`,
+ `"foofoo2"`,
)
teleportContainer.innerHTML = teleportHtml
@@ -342,16 +343,18 @@ describe('SSR hydration', () => {
expect(teleportVnode2.anchor).toBe(container.childNodes[4])
expect(teleportVnode1.target).toBe(teleportContainer)
+ expect(teleportVnode1.targetStart).toBe(teleportContainer.childNodes[0])
expect((teleportVnode1 as any).children[0].el).toBe(
- teleportContainer.childNodes[0],
+ teleportContainer.childNodes[1],
)
- expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[2])
+ expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[3])
expect(teleportVnode2.target).toBe(teleportContainer)
+ expect(teleportVnode2.targetStart).toBe(teleportContainer.childNodes[4])
expect((teleportVnode2 as any).children[0].el).toBe(
- teleportContainer.childNodes[3],
+ teleportContainer.childNodes[5],
)
- expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[5])
+ expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[7])
// // event handler
triggerEvent('click', teleportContainer.querySelector('.foo')!)
@@ -363,7 +366,7 @@ describe('SSR hydration', () => {
msg.value = 'bar'
await nextTick()
expect(teleportContainer.innerHTML).toMatchInlineSnapshot(
- `"barbar2"`,
+ `"barbar2"`,
)
})
@@ -390,7 +393,9 @@ describe('SSR hydration', () => {
)
const teleportHtml = ctx.teleports!['#teleport3']
- expect(teleportHtml).toMatchInlineSnapshot(`""`)
+ expect(teleportHtml).toMatchInlineSnapshot(
+ `""`,
+ )
teleportContainer.innerHTML = teleportHtml
document.body.appendChild(teleportContainer)
@@ -413,7 +418,8 @@ describe('SSR hydration', () => {
expect(children[2].el).toBe(container.childNodes[6])
expect(teleportVnode.target).toBe(teleportContainer)
- expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[0])
+ expect(teleportVnode.targetStart).toBe(teleportContainer.childNodes[0])
+ expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[1])
// // event handler
triggerEvent('click', container.querySelector('.foo')!)
@@ -454,7 +460,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 = {
@@ -483,7 +489,7 @@ describe('SSR hydration', () => {
test('Teleport (nested)', () => {
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport5'
- teleportContainer.innerHTML = `
child
`
+ teleportContainer.innerHTML = `child
`
document.body.appendChild(teleportContainer)
const { vnode, container } = mountWithHydration(
@@ -498,7 +504,7 @@ describe('SSR hydration', () => {
expect(vnode.anchor).toBe(container.lastChild)
const childDivVNode = (vnode as any).children[0]
- const div = teleportContainer.firstChild
+ const div = teleportContainer.childNodes[1]
expect(childDivVNode.el).toBe(div)
expect(vnode.targetAnchor).toBe(div?.nextSibling)
@@ -548,6 +554,66 @@ describe('SSR hydration', () => {
teleportContainer.id = 'target'
document.body.appendChild(teleportContainer)
+ // server render
+ const ctx: SSRContext = {}
+ container.innerHTML = await renderToString(h(App), ctx)
+ expect(container.innerHTML).toBe(
+ '',
+ )
+ teleportContainer.innerHTML = ctx.teleports!['#target']
+
+ // hydrate
+ createSSRApp(App).mount(container)
+ expect(container.innerHTML).toBe(
+ '',
+ )
+ expect(teleportContainer.innerHTML).toBe(
+ 'Teleported Comp1',
+ )
+ expect(`Hydration children mismatch`).not.toHaveBeenWarned()
+
+ toggle.value = false
+ await nextTick()
+ expect(container.innerHTML).toBe('')
+ expect(teleportContainer.innerHTML).toBe('')
+ })
+
+ test('Teleport unmount (mismatch + full integration)', async () => {
+ const Comp1 = {
+ template: `
+
+ Teleported Comp1
+
+ `,
+ }
+ const Comp2 = {
+ template: `
+ Comp2
+ `,
+ }
+
+ const toggle = ref(true)
+ const App = {
+ template: `
+
+
+
+
+ `,
+ components: {
+ Comp1,
+ Comp2,
+ },
+ setup() {
+ return { toggle }
+ },
+ }
+
+ const container = document.createElement('div')
+ const teleportContainer = document.createElement('div')
+ teleportContainer.id = 'target'
+ document.body.appendChild(teleportContainer)
+
// server render
container.innerHTML = await renderToString(h(App))
expect(container.innerHTML).toBe(
@@ -569,7 +635,7 @@ describe('SSR hydration', () => {
expect(teleportContainer.innerHTML).toBe('')
})
- test('Teleport target change (full integration)', async () => {
+ test('Teleport target change (mismatch + full integration)', async () => {
const target = ref('#target1')
const Comp = {
template: `
diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts
index 81573cc85a7..d868fbbc669 100644
--- a/packages/runtime-core/src/components/Teleport.ts
+++ b/packages/runtime-core/src/components/Teleport.ts
@@ -381,7 +381,8 @@ function hydrateTeleport(
slotScopeIds,
optimized,
)
- vnode.targetStart = vnode.targetAnchor = targetNode
+ vnode.targetStart = targetNode
+ vnode.targetAnchor = targetNode && nextSibling(targetNode)
} else {
vnode.anchor = nextSibling(node)
@@ -390,28 +391,29 @@ function hydrateTeleport(
// 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
+ if (targetAnchor && targetAnchor.nodeType === 8) {
+ if ((targetAnchor as Comment).data === 'teleport start anchor') {
+ vnode.targetStart = targetAnchor
+ } else if ((targetAnchor as Comment).data === 'teleport anchor') {
+ vnode.targetAnchor = targetAnchor
+ ;(target as TeleportTargetElement)._lpa =
+ vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
+ break
+ }
}
+ targetAnchor = nextSibling(targetAnchor)
}
- // #11400 if the HTML corresponding to Teleport is not embedded in the correct position
- // on the final page during SSR. the targetAnchor will always be null, we need to
- // manually add targetAnchor to ensure Teleport it can properly unmount or move
+ // #11400 if the HTML corresponding to Teleport is not embedded in the
+ // correct position on the final page during SSR. the targetAnchor will
+ // always be null, we need to manually add targetAnchor to ensure
+ // Teleport it can properly unmount or move
if (!vnode.targetAnchor) {
prepareAnchor(target, vnode, createText, insert)
}
hydrateChildren(
- targetNode,
+ targetNode && nextSibling(targetNode),
vnode,
target,
parentComponent,
diff --git a/packages/server-renderer/__tests__/ssrTeleport.spec.ts b/packages/server-renderer/__tests__/ssrTeleport.spec.ts
index a0a2f6ae0a1..78c56942636 100644
--- a/packages/server-renderer/__tests__/ssrTeleport.spec.ts
+++ b/packages/server-renderer/__tests__/ssrTeleport.spec.ts
@@ -28,7 +28,7 @@ describe('ssrRenderTeleport', () => {
)
expect(html).toBe('')
expect(ctx.teleports!['#target']).toBe(
- `content
`,
+ `content
`,
)
})
@@ -56,7 +56,9 @@ describe('ssrRenderTeleport', () => {
expect(html).toBe(
'content
',
)
- expect(ctx.teleports!['#target']).toBe(``)
+ expect(ctx.teleports!['#target']).toBe(
+ ``,
+ )
})
test('teleport rendering (vnode)', async () => {
@@ -73,7 +75,7 @@ describe('ssrRenderTeleport', () => {
)
expect(html).toBe('')
expect(ctx.teleports!['#target']).toBe(
- 'hello',
+ 'hello',
)
})
@@ -93,7 +95,9 @@ 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,8 @@ describe('ssrRenderTeleport', () => {
'',
)
expect(ctx.teleports!['#target']).toBe(
- 'helloworld',
+ 'hello' +
+ 'world',
)
})
@@ -134,7 +139,7 @@ describe('ssrRenderTeleport', () => {
)
expect(html).toBe('')
expect(ctx.teleports!['#target']).toBe(
- `content
`,
+ `content
`,
)
})
@@ -169,7 +174,7 @@ describe('ssrRenderTeleport', () => {
await p
expect(html).toBe('')
expect(ctx.teleports!['#target']).toBe(
- `content
`,
+ `content
`,
)
})
})
diff --git a/packages/server-renderer/src/helpers/ssrRenderTeleport.ts b/packages/server-renderer/src/helpers/ssrRenderTeleport.ts
index d83af28c131..0806a3927be 100644
--- a/packages/server-renderer/src/helpers/ssrRenderTeleport.ts
+++ b/packages/server-renderer/src/helpers/ssrRenderTeleport.ts
@@ -29,9 +29,10 @@ export function ssrRenderTeleport(
if (disabled) {
contentRenderFn(parentPush)
- teleportContent = ``
+ teleportContent = ``
} else {
const { getBuffer, push } = createBuffer()
+ push(``)
contentRenderFn(push)
push(``)
teleportContent = getBuffer()