Skip to content

Commit d7bcd85

Browse files
authored
fix(teleport): properly handling disabled teleport target anchor (#14417)
close #14412
1 parent c09d41f commit d7bcd85

File tree

2 files changed

+114
-35
lines changed

2 files changed

+114
-35
lines changed

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,77 @@ describe('SSR hydration', () => {
683683
expect(teleportContainer.innerHTML).toBe('')
684684
})
685685

686+
test('Teleport unmount (disabled + full integration)', async () => {
687+
const disabled = ref(true)
688+
const target = ref('#teleport001')
689+
const toggle = ref(true)
690+
691+
const Comp = {
692+
template: `
693+
<div>
694+
<div id="teleport001">
695+
<Teleport
696+
:to="target"
697+
:disabled="disabled"
698+
>
699+
<template v-for="section in order">
700+
<div>{{section}}</div>
701+
</template>
702+
</Teleport>
703+
</div>
704+
<div id="teleport002"></div>
705+
</div>
706+
`,
707+
setup() {
708+
const order = ref(['A', 'B', 'C'])
709+
return { target, disabled, order }
710+
},
711+
}
712+
const App = {
713+
template: `<Comp v-if="toggle"/>`,
714+
components: {
715+
Comp,
716+
},
717+
setup() {
718+
return { toggle }
719+
},
720+
}
721+
722+
const container = document.createElement('div')
723+
document.body.appendChild(container)
724+
725+
// server render
726+
container.innerHTML = await renderToString(h(App))
727+
expect(container.innerHTML).toBe(
728+
`<div>` +
729+
`<div id="teleport001">` +
730+
`<!--teleport start-->` +
731+
`<!--[--><div>A</div><div>B</div><div>C</div><!--]-->` +
732+
`<!--teleport end-->` +
733+
`</div>` +
734+
`<div id="teleport002"></div>` +
735+
`</div>`,
736+
)
737+
738+
// hydrate
739+
createSSRApp(App).mount(container)
740+
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
741+
742+
target.value = '#teleport002'
743+
disabled.value = false
744+
await nextTick()
745+
expect(container.querySelector('#teleport001')!.innerHTML).toBe(
746+
'<!--teleport start--><!--teleport end-->',
747+
)
748+
expect(container.querySelector('#teleport002')!.innerHTML).toBe(
749+
'<!--[--><div>A</div><div>B</div><div>C</div><!--]-->',
750+
)
751+
752+
toggle.value = false
753+
await nextTick()
754+
expect(container.innerHTML).toBe('<!--v-if-->')
755+
})
756+
686757
test('Teleport target change (mismatch + full integration)', async () => {
687758
const target = ref('#target1')
688759
const Comp = {

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

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -412,12 +412,30 @@ function hydrateTeleport(
412412
optimized: boolean,
413413
) => Node | null,
414414
): Node | null {
415-
function hydrateDisabledTeleport(
416-
node: Node,
417-
vnode: VNode,
418-
targetStart: Node | null,
419-
targetAnchor: Node | null,
415+
// lookahead until we find the target anchor
416+
// we cannot rely on return value of hydrateChildren() because there
417+
// could be nested teleports
418+
function hydrateAnchor(
419+
target: TeleportTargetElement,
420+
targetNode: Node | null,
420421
) {
422+
let targetAnchor = targetNode
423+
while (targetAnchor) {
424+
if (targetAnchor && targetAnchor.nodeType === 8) {
425+
if ((targetAnchor as Comment).data === 'teleport start anchor') {
426+
vnode.targetStart = targetAnchor
427+
} else if ((targetAnchor as Comment).data === 'teleport anchor') {
428+
vnode.targetAnchor = targetAnchor
429+
target._lpa =
430+
vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
431+
break
432+
}
433+
}
434+
targetAnchor = nextSibling(targetAnchor)
435+
}
436+
}
437+
438+
function hydrateDisabledTeleport(node: Node, vnode: VNode) {
421439
vnode.anchor = hydrateChildren(
422440
nextSibling(node),
423441
vnode,
@@ -427,8 +445,6 @@ function hydrateTeleport(
427445
slotScopeIds,
428446
optimized,
429447
)
430-
vnode.targetStart = targetStart
431-
vnode.targetAnchor = targetAnchor
432448
}
433449

434450
const target = (vnode.target = resolveTarget<Element>(
@@ -443,33 +459,22 @@ function hydrateTeleport(
443459
(target as TeleportTargetElement)._lpa || target.firstChild
444460
if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
445461
if (disabled) {
446-
hydrateDisabledTeleport(
447-
node,
448-
vnode,
449-
targetNode,
450-
targetNode && nextSibling(targetNode),
451-
)
462+
hydrateDisabledTeleport(node, vnode)
463+
hydrateAnchor(target as TeleportTargetElement, targetNode)
464+
if (!vnode.targetAnchor) {
465+
prepareAnchor(
466+
target,
467+
vnode,
468+
createText,
469+
insert,
470+
// if target is the same as the main view, insert anchors before current node
471+
// to avoid hydrating mismatch
472+
parentNode(node)! === target ? node : null,
473+
)
474+
}
452475
} else {
453476
vnode.anchor = nextSibling(node)
454-
455-
// lookahead until we find the target anchor
456-
// we cannot rely on return value of hydrateChildren() because there
457-
// could be nested teleports
458-
let targetAnchor = targetNode
459-
while (targetAnchor) {
460-
if (targetAnchor && targetAnchor.nodeType === 8) {
461-
if ((targetAnchor as Comment).data === 'teleport start anchor') {
462-
vnode.targetStart = targetAnchor
463-
} else if ((targetAnchor as Comment).data === 'teleport anchor') {
464-
vnode.targetAnchor = targetAnchor
465-
;(target as TeleportTargetElement)._lpa =
466-
vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
467-
break
468-
}
469-
}
470-
targetAnchor = nextSibling(targetAnchor)
471-
}
472-
477+
hydrateAnchor(target as TeleportTargetElement, targetNode)
473478
// #11400 if the HTML corresponding to Teleport is not embedded in the
474479
// correct position on the final page during SSR. the targetAnchor will
475480
// always be null, we need to manually add targetAnchor to ensure
@@ -492,7 +497,9 @@ function hydrateTeleport(
492497
updateCssVars(vnode, disabled)
493498
} else if (disabled) {
494499
if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
495-
hydrateDisabledTeleport(node, vnode, node, nextSibling(node))
500+
hydrateDisabledTeleport(node, vnode)
501+
vnode.targetStart = node
502+
vnode.targetAnchor = nextSibling(node)
496503
}
497504
}
498505
return vnode.anchor && nextSibling(vnode.anchor as Node)
@@ -535,6 +542,7 @@ function prepareAnchor(
535542
vnode: TeleportVNode,
536543
createText: RendererOptions['createText'],
537544
insert: RendererOptions['insert'],
545+
anchor: RendererNode | null = null,
538546
) {
539547
const targetStart = (vnode.targetStart = createText(''))
540548
const targetAnchor = (vnode.targetAnchor = createText(''))
@@ -544,8 +552,8 @@ function prepareAnchor(
544552
targetStart[TeleportEndKey] = targetAnchor
545553

546554
if (target) {
547-
insert(targetStart, target)
548-
insert(targetAnchor, target)
555+
insert(targetStart, target, anchor)
556+
insert(targetAnchor, target, anchor)
549557
}
550558

551559
return targetAnchor

0 commit comments

Comments
 (0)