Skip to content

Commit 12667da

Browse files
authoredJul 31, 2024··
fix(Teleport): ensure targetAnchor and targetStart not null during hydration (#11456)
close #11400
1 parent af60e35 commit 12667da

File tree

2 files changed

+144
-10
lines changed

2 files changed

+144
-10
lines changed
 

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

+112
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,118 @@ describe('SSR hydration', () => {
512512
)
513513
})
514514

515+
test('Teleport unmount (full integration)', async () => {
516+
const Comp1 = {
517+
template: `
518+
<Teleport to="#target">
519+
<span>Teleported Comp1</span>
520+
</Teleport>
521+
`,
522+
}
523+
const Comp2 = {
524+
template: `
525+
<div>Comp2</div>
526+
`,
527+
}
528+
529+
const toggle = ref(true)
530+
const App = {
531+
template: `
532+
<div>
533+
<Comp1 v-if="toggle"/>
534+
<Comp2 v-else/>
535+
</div>
536+
`,
537+
components: {
538+
Comp1,
539+
Comp2,
540+
},
541+
setup() {
542+
return { toggle }
543+
},
544+
}
545+
546+
const container = document.createElement('div')
547+
const teleportContainer = document.createElement('div')
548+
teleportContainer.id = 'target'
549+
document.body.appendChild(teleportContainer)
550+
551+
// server render
552+
container.innerHTML = await renderToString(h(App))
553+
expect(container.innerHTML).toBe(
554+
'<div><!--teleport start--><!--teleport end--></div>',
555+
)
556+
expect(teleportContainer.innerHTML).toBe('')
557+
558+
// hydrate
559+
createSSRApp(App).mount(container)
560+
expect(container.innerHTML).toBe(
561+
'<div><!--teleport start--><!--teleport end--></div>',
562+
)
563+
expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>')
564+
expect(`Hydration children mismatch`).toHaveBeenWarned()
565+
566+
toggle.value = false
567+
await nextTick()
568+
expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
569+
expect(teleportContainer.innerHTML).toBe('')
570+
})
571+
572+
test('Teleport target change (full integration)', async () => {
573+
const target = ref('#target1')
574+
const Comp = {
575+
template: `
576+
<Teleport :to="target">
577+
<span>Teleported</span>
578+
</Teleport>
579+
`,
580+
setup() {
581+
return { target }
582+
},
583+
}
584+
585+
const App = {
586+
template: `
587+
<div>
588+
<Comp />
589+
</div>
590+
`,
591+
components: {
592+
Comp,
593+
},
594+
}
595+
596+
const container = document.createElement('div')
597+
const teleportContainer1 = document.createElement('div')
598+
teleportContainer1.id = 'target1'
599+
const teleportContainer2 = document.createElement('div')
600+
teleportContainer2.id = 'target2'
601+
document.body.appendChild(teleportContainer1)
602+
document.body.appendChild(teleportContainer2)
603+
604+
// server render
605+
container.innerHTML = await renderToString(h(App))
606+
expect(container.innerHTML).toBe(
607+
'<div><!--teleport start--><!--teleport end--></div>',
608+
)
609+
expect(teleportContainer1.innerHTML).toBe('')
610+
expect(teleportContainer2.innerHTML).toBe('')
611+
612+
// hydrate
613+
createSSRApp(App).mount(container)
614+
expect(container.innerHTML).toBe(
615+
'<div><!--teleport start--><!--teleport end--></div>',
616+
)
617+
expect(teleportContainer1.innerHTML).toBe('<span>Teleported</span>')
618+
expect(teleportContainer2.innerHTML).toBe('')
619+
expect(`Hydration children mismatch`).toHaveBeenWarned()
620+
621+
target.value = '#target2'
622+
await nextTick()
623+
expect(teleportContainer1.innerHTML).toBe('')
624+
expect(teleportContainer2.innerHTML).toBe('<span>Teleported</span>')
625+
})
626+
515627
// compile SSR + client render fn from the same template & hydrate
516628
test('full compiler integration', async () => {
517629
const mounted: string[] = []

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

+32-10
Original file line numberDiff line numberDiff line change
@@ -107,17 +107,11 @@ export const TeleportImpl = {
107107
const mainAnchor = (n2.anchor = __DEV__
108108
? createComment('teleport end')
109109
: createText(''))
110-
const target = (n2.target = resolveTarget(n2.props, querySelector))
111-
const targetStart = (n2.targetStart = createText(''))
112-
const targetAnchor = (n2.targetAnchor = createText(''))
113110
insert(placeholder, container, anchor)
114111
insert(mainAnchor, container, anchor)
115-
// attach a special property so we can skip teleported content in
116-
// renderer's nextSibling search
117-
targetStart[TeleportEndKey] = targetAnchor
112+
const target = (n2.target = resolveTarget(n2.props, querySelector))
113+
const targetAnchor = prepareAnchor(target, n2, createText, insert)
118114
if (target) {
119-
insert(targetStart, target)
120-
insert(targetAnchor, target)
121115
// #2652 we could be teleporting from a non-SVG tree into an SVG tree
122116
if (namespace === 'svg' || isTargetSVG(target)) {
123117
namespace = 'svg'
@@ -355,7 +349,7 @@ function hydrateTeleport(
355349
slotScopeIds: string[] | null,
356350
optimized: boolean,
357351
{
358-
o: { nextSibling, parentNode, querySelector },
352+
o: { nextSibling, parentNode, querySelector, insert, createText },
359353
}: RendererInternals<Node, Element>,
360354
hydrateChildren: (
361355
node: Node | null,
@@ -387,7 +381,7 @@ function hydrateTeleport(
387381
slotScopeIds,
388382
optimized,
389383
)
390-
vnode.targetAnchor = targetNode
384+
vnode.targetStart = vnode.targetAnchor = targetNode
391385
} else {
392386
vnode.anchor = nextSibling(node)
393387

@@ -409,6 +403,13 @@ function hydrateTeleport(
409403
}
410404
}
411405

406+
// #11400 if the HTML corresponding to Teleport is not embedded in the correct position
407+
// on the final page during SSR. the targetAnchor will always be null, we need to
408+
// manually add targetAnchor to ensure Teleport it can properly unmount or move
409+
if (!vnode.targetAnchor) {
410+
prepareAnchor(target, vnode, createText, insert)
411+
}
412+
412413
hydrateChildren(
413414
targetNode,
414415
vnode,
@@ -449,3 +450,24 @@ function updateCssVars(vnode: VNode) {
449450
ctx.ut()
450451
}
451452
}
453+
454+
function prepareAnchor(
455+
target: RendererElement | null,
456+
vnode: TeleportVNode,
457+
createText: RendererOptions['createText'],
458+
insert: RendererOptions['insert'],
459+
) {
460+
const targetStart = (vnode.targetStart = createText(''))
461+
const targetAnchor = (vnode.targetAnchor = createText(''))
462+
463+
// attach a special property, so we can skip teleported content in
464+
// renderer's nextSibling search
465+
targetStart[TeleportEndKey] = targetAnchor
466+
467+
if (target) {
468+
insert(targetStart, target)
469+
insert(targetAnchor, target)
470+
}
471+
472+
return targetAnchor
473+
}

0 commit comments

Comments
 (0)
Please sign in to comment.