Skip to content

Commit

Permalink
fix(Teleport): ensure targetAnchor and targetStart not null during hy…
Browse files Browse the repository at this point in the history
…dration (#11456)

close #11400
  • Loading branch information
edison1105 authored Jul 31, 2024
1 parent af60e35 commit 12667da
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 10 deletions.
112 changes: 112 additions & 0 deletions packages/runtime-core/__tests__/hydration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,118 @@ describe('SSR hydration', () => {
)
})

test('Teleport unmount (full integration)', async () => {
const Comp1 = {
template: `
<Teleport to="#target">
<span>Teleported Comp1</span>
</Teleport>
`,
}
const Comp2 = {
template: `
<div>Comp2</div>
`,
}

const toggle = ref(true)
const App = {
template: `
<div>
<Comp1 v-if="toggle"/>
<Comp2 v-else/>
</div>
`,
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(
'<div><!--teleport start--><!--teleport end--></div>',
)
expect(teleportContainer.innerHTML).toBe('')

// hydrate
createSSRApp(App).mount(container)
expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--></div>',
)
expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>')
expect(`Hydration children mismatch`).toHaveBeenWarned()

toggle.value = false
await nextTick()
expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
expect(teleportContainer.innerHTML).toBe('')
})

test('Teleport target change (full integration)', async () => {
const target = ref('#target1')
const Comp = {
template: `
<Teleport :to="target">
<span>Teleported</span>
</Teleport>
`,
setup() {
return { target }
},
}

const App = {
template: `
<div>
<Comp />
</div>
`,
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(
'<div><!--teleport start--><!--teleport end--></div>',
)
expect(teleportContainer1.innerHTML).toBe('')
expect(teleportContainer2.innerHTML).toBe('')

// hydrate
createSSRApp(App).mount(container)
expect(container.innerHTML).toBe(
'<div><!--teleport start--><!--teleport end--></div>',
)
expect(teleportContainer1.innerHTML).toBe('<span>Teleported</span>')
expect(teleportContainer2.innerHTML).toBe('')
expect(`Hydration children mismatch`).toHaveBeenWarned()

target.value = '#target2'
await nextTick()
expect(teleportContainer1.innerHTML).toBe('')
expect(teleportContainer2.innerHTML).toBe('<span>Teleported</span>')
})

// compile SSR + client render fn from the same template & hydrate
test('full compiler integration', async () => {
const mounted: string[] = []
Expand Down
42 changes: 32 additions & 10 deletions packages/runtime-core/src/components/Teleport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -355,7 +349,7 @@ function hydrateTeleport(
slotScopeIds: string[] | null,
optimized: boolean,
{
o: { nextSibling, parentNode, querySelector },
o: { nextSibling, parentNode, querySelector, insert, createText },
}: RendererInternals<Node, Element>,
hydrateChildren: (
node: Node | null,
Expand Down Expand Up @@ -387,7 +381,7 @@ function hydrateTeleport(
slotScopeIds,
optimized,
)
vnode.targetAnchor = targetNode
vnode.targetStart = vnode.targetAnchor = targetNode
} else {
vnode.anchor = nextSibling(node)

Expand All @@ -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,
Expand Down Expand Up @@ -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
}

0 comments on commit 12667da

Please sign in to comment.