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('
Comp2
') + 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 +}