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
|
// compile SSR + client render fn from the same template & hydrate
|
||||||
test('full compiler integration', async () => {
|
test('full compiler integration', async () => {
|
||||||
const mounted: string[] = []
|
const mounted: string[] = []
|
||||||
|
|
|
@ -107,17 +107,11 @@ export const TeleportImpl = {
|
||||||
const mainAnchor = (n2.anchor = __DEV__
|
const mainAnchor = (n2.anchor = __DEV__
|
||||||
? createComment('teleport end')
|
? createComment('teleport end')
|
||||||
: createText(''))
|
: createText(''))
|
||||||
const target = (n2.target = resolveTarget(n2.props, querySelector))
|
|
||||||
const targetStart = (n2.targetStart = createText(''))
|
|
||||||
const targetAnchor = (n2.targetAnchor = createText(''))
|
|
||||||
insert(placeholder, container, anchor)
|
insert(placeholder, container, anchor)
|
||||||
insert(mainAnchor, container, anchor)
|
insert(mainAnchor, container, anchor)
|
||||||
// attach a special property so we can skip teleported content in
|
const target = (n2.target = resolveTarget(n2.props, querySelector))
|
||||||
// renderer's nextSibling search
|
const targetAnchor = prepareAnchor(target, n2, createText, insert)
|
||||||
targetStart[TeleportEndKey] = targetAnchor
|
|
||||||
if (target) {
|
if (target) {
|
||||||
insert(targetStart, target)
|
|
||||||
insert(targetAnchor, target)
|
|
||||||
// #2652 we could be teleporting from a non-SVG tree into an SVG tree
|
// #2652 we could be teleporting from a non-SVG tree into an SVG tree
|
||||||
if (namespace === 'svg' || isTargetSVG(target)) {
|
if (namespace === 'svg' || isTargetSVG(target)) {
|
||||||
namespace = 'svg'
|
namespace = 'svg'
|
||||||
|
@ -355,7 +349,7 @@ function hydrateTeleport(
|
||||||
slotScopeIds: string[] | null,
|
slotScopeIds: string[] | null,
|
||||||
optimized: boolean,
|
optimized: boolean,
|
||||||
{
|
{
|
||||||
o: { nextSibling, parentNode, querySelector },
|
o: { nextSibling, parentNode, querySelector, insert, createText },
|
||||||
}: RendererInternals<Node, Element>,
|
}: RendererInternals<Node, Element>,
|
||||||
hydrateChildren: (
|
hydrateChildren: (
|
||||||
node: Node | null,
|
node: Node | null,
|
||||||
|
@ -387,7 +381,7 @@ function hydrateTeleport(
|
||||||
slotScopeIds,
|
slotScopeIds,
|
||||||
optimized,
|
optimized,
|
||||||
)
|
)
|
||||||
vnode.targetAnchor = targetNode
|
vnode.targetStart = vnode.targetAnchor = targetNode
|
||||||
} else {
|
} else {
|
||||||
vnode.anchor = nextSibling(node)
|
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(
|
hydrateChildren(
|
||||||
targetNode,
|
targetNode,
|
||||||
vnode,
|
vnode,
|
||||||
|
@ -449,3 +450,24 @@ function updateCssVars(vnode: VNode) {
|
||||||
ctx.ut()
|
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