mirror of https://github.com/vuejs/core.git
fix(Teleport): ensure targetAnchor and targetStart not null during hydration (#11456)
close #11400
This commit is contained in:
parent
af60e3560c
commit
12667da487
|
@ -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[] = []
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue