diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts
index ea1d626f7c4..9916fafa62c 100644
--- a/packages/runtime-core/__tests__/hydration.spec.ts
+++ b/packages/runtime-core/__tests__/hydration.spec.ts
@@ -512,6 +512,118 @@ describe('SSR hydration', () => {
)
})
+ test('Teleport unmount (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(
+ '',
+ )
+ expect(teleportContainer.innerHTML).toBe('')
+
+ // hydrate
+ createSSRApp(App).mount(container)
+ expect(container.innerHTML).toBe(
+ '',
+ )
+ expect(teleportContainer.innerHTML).toBe('Teleported Comp1')
+ expect(`Hydration children mismatch`).toHaveBeenWarned()
+
+ toggle.value = false
+ await nextTick()
+ expect(container.innerHTML).toBe('')
+ expect(teleportContainer.innerHTML).toBe('')
+ })
+
+ test('Teleport target change (full integration)', async () => {
+ const target = ref('#target1')
+ const Comp = {
+ template: `
+
+ Teleported
+
+ `,
+ setup() {
+ return { target }
+ },
+ }
+
+ const App = {
+ template: `
+
+
+
+ `,
+ components: {
+ Comp,
+ },
+ }
+
+ const container = document.createElement('div')
+ const teleportContainer1 = document.createElement('div')
+ teleportContainer1.id = 'target1'
+ const teleportContainer2 = document.createElement('div')
+ teleportContainer2.id = 'target2'
+ document.body.appendChild(teleportContainer1)
+ document.body.appendChild(teleportContainer2)
+
+ // server render
+ container.innerHTML = await renderToString(h(App))
+ expect(container.innerHTML).toBe(
+ '',
+ )
+ expect(teleportContainer1.innerHTML).toBe('')
+ expect(teleportContainer2.innerHTML).toBe('')
+
+ // hydrate
+ createSSRApp(App).mount(container)
+ expect(container.innerHTML).toBe(
+ '',
+ )
+ expect(teleportContainer1.innerHTML).toBe('Teleported')
+ expect(teleportContainer2.innerHTML).toBe('')
+ expect(`Hydration children mismatch`).toHaveBeenWarned()
+
+ target.value = '#target2'
+ await nextTick()
+ expect(teleportContainer1.innerHTML).toBe('')
+ expect(teleportContainer2.innerHTML).toBe('Teleported')
+ })
+
// 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 65437300cff..81573cc85a7 100644
--- a/packages/runtime-core/src/components/Teleport.ts
+++ b/packages/runtime-core/src/components/Teleport.ts
@@ -107,17 +107,11 @@ export const TeleportImpl = {
const mainAnchor = (n2.anchor = __DEV__
? createComment('teleport end')
: createText(''))
- const target = (n2.target = resolveTarget(n2.props, querySelector))
- const targetStart = (n2.targetStart = createText(''))
- const targetAnchor = (n2.targetAnchor = createText(''))
insert(placeholder, container, anchor)
insert(mainAnchor, container, anchor)
- // attach a special property so we can skip teleported content in
- // renderer's nextSibling search
- targetStart[TeleportEndKey] = targetAnchor
+ const target = (n2.target = resolveTarget(n2.props, querySelector))
+ const targetAnchor = prepareAnchor(target, n2, createText, insert)
if (target) {
- insert(targetStart, target)
- insert(targetAnchor, target)
// #2652 we could be teleporting from a non-SVG tree into an SVG tree
if (namespace === 'svg' || isTargetSVG(target)) {
namespace = 'svg'
@@ -355,7 +349,7 @@ function hydrateTeleport(
slotScopeIds: string[] | null,
optimized: boolean,
{
- o: { nextSibling, parentNode, querySelector },
+ o: { nextSibling, parentNode, querySelector, insert, createText },
}: RendererInternals,
hydrateChildren: (
node: Node | null,
@@ -387,7 +381,7 @@ function hydrateTeleport(
slotScopeIds,
optimized,
)
- vnode.targetAnchor = targetNode
+ vnode.targetStart = vnode.targetAnchor = targetNode
} else {
vnode.anchor = nextSibling(node)
@@ -409,6 +403,13 @@ function hydrateTeleport(
}
}
+ // #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,
vnode,
@@ -449,3 +450,24 @@ function updateCssVars(vnode: VNode) {
ctx.ut()
}
}
+
+function prepareAnchor(
+ target: RendererElement | null,
+ vnode: TeleportVNode,
+ createText: RendererOptions['createText'],
+ insert: RendererOptions['insert'],
+) {
+ const targetStart = (vnode.targetStart = createText(''))
+ const targetAnchor = (vnode.targetAnchor = createText(''))
+
+ // attach a special property, so we can skip teleported content in
+ // renderer's nextSibling search
+ targetStart[TeleportEndKey] = targetAnchor
+
+ if (target) {
+ insert(targetStart, target)
+ insert(targetAnchor, target)
+ }
+
+ return targetAnchor
+}