fix(Teleport): ensure targetAnchor and targetStart not null during hydration (#11456)

close #11400
This commit is contained in:
edison 2024-07-31 15:46:39 +08:00 committed by GitHub
parent af60e3560c
commit 12667da487
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 144 additions and 10 deletions

View File

@ -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[] = []

View File

@ -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<Node, Element>,
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
}