Skip to content

Commit d3c6e02

Browse files
authored
fix(teleport): move targetStart and keep children before targetAnchor (#15818)
fix #15817
1 parent 0f8e7b4 commit d3c6e02

2 files changed

Lines changed: 86 additions & 1 deletion

File tree

‎packages/runtime-core/__tests__/components/Teleport.spec.ts‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,83 @@ describe('renderer: teleport', () => {
540540
expect(serializeInner(targetB)).toBe(`<div>teleported</div>`)
541541
})
542542

543+
test('should move targetStart and keep appended children in order after target change', async () => {
544+
const root = document.createElement('div')
545+
const targetA = document.createElement('div')
546+
const targetB = document.createElement('div')
547+
document.body.append(root, targetA, targetB)
548+
549+
const target = ref<HTMLElement>(targetA)
550+
const showB = ref(false)
551+
552+
domRender(
553+
h(() =>
554+
h(
555+
Teleport,
556+
{ to: target.value },
557+
showB.value ? [h('div', 'A'), h('div', 'B')] : [h('div', 'A')],
558+
),
559+
),
560+
root,
561+
)
562+
await nextTick()
563+
564+
expect(targetA.innerHTML).toBe(`<div>A</div>`)
565+
566+
target.value = targetB
567+
await nextTick()
568+
569+
// targetStart must not be left behind in the old target
570+
expect(targetA.childNodes.length).toBe(0)
571+
// targetStart, A, targetAnchor
572+
expect(targetB.childNodes.length).toBe(3)
573+
574+
showB.value = true
575+
await nextTick()
576+
577+
expect(targetB.innerHTML).toBe(`<div>A</div><div>B</div>`)
578+
579+
domRender(null, root)
580+
root.remove()
581+
targetA.remove()
582+
targetB.remove()
583+
})
584+
585+
test('should not crash when root is replaced after target changed from own container', async () => {
586+
const root = document.createElement('div')
587+
const targetB = document.createElement('div')
588+
document.body.append(root, targetB)
589+
590+
const target = ref<HTMLElement>(root)
591+
const swapped = ref(false)
592+
593+
const Comp = {
594+
render: () =>
595+
swapped.value
596+
? h('div', 'swapped')
597+
: [
598+
h(Teleport, { to: target.value }, [h('div', 'A')]),
599+
h('div', 'x'),
600+
],
601+
}
602+
603+
domRender(h(Comp), root)
604+
await nextTick()
605+
606+
target.value = targetB
607+
await nextTick()
608+
609+
swapped.value = true
610+
await nextTick()
611+
612+
expect(root.innerHTML).toBe(`<div>swapped</div>`)
613+
expect(targetB.childNodes.length).toBe(0)
614+
615+
domRender(null, root)
616+
root.remove()
617+
targetB.remove()
618+
})
619+
543620
test('move cached text nodes', async () => {
544621
document.body.innerHTML = ''
545622
const root = document.createElement('div')

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,9 +382,17 @@ function moveTeleport(
382382
{ o: { insert }, m: move }: RendererInternals,
383383
moveType: TeleportMoveTypes = TeleportMoveTypes.REORDER,
384384
): void {
385-
// move target anchor if this is a target change.
385+
// move target anchors if this is a target change.
386386
if (moveType === TeleportMoveTypes.TARGET_CHANGE) {
387+
// targetStart is only set by hydration when the start anchor is found
388+
if (vnode.targetStart) {
389+
insert(vnode.targetStart, container, parentAnchor)
390+
}
387391
insert(vnode.targetAnchor!, container, parentAnchor)
392+
// children have to stay between the two anchors, otherwise subsequently
393+
// appended children are mounted before targetAnchor while the moved ones
394+
// sit after it
395+
parentAnchor = vnode.targetAnchor!
388396
}
389397
const { el, anchor, shapeFlag, children, props } = vnode
390398
const isReorder = moveType === TeleportMoveTypes.REORDER

0 commit comments

Comments
 (0)