mirror of https://github.com/vuejs/core.git
fix(teleport/ssr): fix Teleport hydration regression due to targetStart anchor addition
This commit is contained in:
parent
12667da487
commit
7b18cdb0b5
|
@ -265,7 +265,7 @@ describe('SSR hydration', () => {
|
||||||
const fn = vi.fn()
|
const fn = vi.fn()
|
||||||
const teleportContainer = document.createElement('div')
|
const teleportContainer = document.createElement('div')
|
||||||
teleportContainer.id = 'teleport'
|
teleportContainer.id = 'teleport'
|
||||||
teleportContainer.innerHTML = `<span>foo</span><span class="foo"></span><!--teleport anchor-->`
|
teleportContainer.innerHTML = `<!--teleport start anchor--><span>foo</span><span class="foo"></span><!--teleport anchor-->`
|
||||||
document.body.appendChild(teleportContainer)
|
document.body.appendChild(teleportContainer)
|
||||||
|
|
||||||
const { vnode, container } = mountWithHydration(
|
const { vnode, container } = mountWithHydration(
|
||||||
|
@ -281,13 +281,14 @@ describe('SSR hydration', () => {
|
||||||
expect(vnode.anchor).toBe(container.lastChild)
|
expect(vnode.anchor).toBe(container.lastChild)
|
||||||
|
|
||||||
expect(vnode.target).toBe(teleportContainer)
|
expect(vnode.target).toBe(teleportContainer)
|
||||||
|
expect(vnode.targetStart).toBe(teleportContainer.childNodes[0])
|
||||||
expect((vnode.children as VNode[])[0].el).toBe(
|
expect((vnode.children as VNode[])[0].el).toBe(
|
||||||
teleportContainer.childNodes[0],
|
|
||||||
)
|
|
||||||
expect((vnode.children as VNode[])[1].el).toBe(
|
|
||||||
teleportContainer.childNodes[1],
|
teleportContainer.childNodes[1],
|
||||||
)
|
)
|
||||||
expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[2])
|
expect((vnode.children as VNode[])[1].el).toBe(
|
||||||
|
teleportContainer.childNodes[2],
|
||||||
|
)
|
||||||
|
expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[3])
|
||||||
|
|
||||||
// event handler
|
// event handler
|
||||||
triggerEvent('click', teleportContainer.querySelector('.foo')!)
|
triggerEvent('click', teleportContainer.querySelector('.foo')!)
|
||||||
|
@ -296,7 +297,7 @@ describe('SSR hydration', () => {
|
||||||
msg.value = 'bar'
|
msg.value = 'bar'
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(teleportContainer.innerHTML).toBe(
|
expect(teleportContainer.innerHTML).toBe(
|
||||||
`<span>bar</span><span class="bar"></span><!--teleport anchor-->`,
|
`<!--teleport start anchor--><span>bar</span><span class="bar"></span><!--teleport anchor-->`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -326,7 +327,7 @@ describe('SSR hydration', () => {
|
||||||
|
|
||||||
const teleportHtml = ctx.teleports!['#teleport2']
|
const teleportHtml = ctx.teleports!['#teleport2']
|
||||||
expect(teleportHtml).toMatchInlineSnapshot(
|
expect(teleportHtml).toMatchInlineSnapshot(
|
||||||
`"<span>foo</span><span class="foo"></span><!--teleport anchor--><span>foo2</span><span class="foo2"></span><!--teleport anchor-->"`,
|
`"<!--teleport start anchor--><span>foo</span><span class="foo"></span><!--teleport anchor--><!--teleport start anchor--><span>foo2</span><span class="foo2"></span><!--teleport anchor-->"`,
|
||||||
)
|
)
|
||||||
|
|
||||||
teleportContainer.innerHTML = teleportHtml
|
teleportContainer.innerHTML = teleportHtml
|
||||||
|
@ -342,16 +343,18 @@ describe('SSR hydration', () => {
|
||||||
expect(teleportVnode2.anchor).toBe(container.childNodes[4])
|
expect(teleportVnode2.anchor).toBe(container.childNodes[4])
|
||||||
|
|
||||||
expect(teleportVnode1.target).toBe(teleportContainer)
|
expect(teleportVnode1.target).toBe(teleportContainer)
|
||||||
|
expect(teleportVnode1.targetStart).toBe(teleportContainer.childNodes[0])
|
||||||
expect((teleportVnode1 as any).children[0].el).toBe(
|
expect((teleportVnode1 as any).children[0].el).toBe(
|
||||||
teleportContainer.childNodes[0],
|
teleportContainer.childNodes[1],
|
||||||
)
|
)
|
||||||
expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[2])
|
expect(teleportVnode1.targetAnchor).toBe(teleportContainer.childNodes[3])
|
||||||
|
|
||||||
expect(teleportVnode2.target).toBe(teleportContainer)
|
expect(teleportVnode2.target).toBe(teleportContainer)
|
||||||
|
expect(teleportVnode2.targetStart).toBe(teleportContainer.childNodes[4])
|
||||||
expect((teleportVnode2 as any).children[0].el).toBe(
|
expect((teleportVnode2 as any).children[0].el).toBe(
|
||||||
teleportContainer.childNodes[3],
|
teleportContainer.childNodes[5],
|
||||||
)
|
)
|
||||||
expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[5])
|
expect(teleportVnode2.targetAnchor).toBe(teleportContainer.childNodes[7])
|
||||||
|
|
||||||
// // event handler
|
// // event handler
|
||||||
triggerEvent('click', teleportContainer.querySelector('.foo')!)
|
triggerEvent('click', teleportContainer.querySelector('.foo')!)
|
||||||
|
@ -363,7 +366,7 @@ describe('SSR hydration', () => {
|
||||||
msg.value = 'bar'
|
msg.value = 'bar'
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(teleportContainer.innerHTML).toMatchInlineSnapshot(
|
expect(teleportContainer.innerHTML).toMatchInlineSnapshot(
|
||||||
`"<span>bar</span><span class="bar"></span><!--teleport anchor--><span>bar2</span><span class="bar2"></span><!--teleport anchor-->"`,
|
`"<!--teleport start anchor--><span>bar</span><span class="bar"></span><!--teleport anchor--><!--teleport start anchor--><span>bar2</span><span class="bar2"></span><!--teleport anchor-->"`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -390,7 +393,9 @@ describe('SSR hydration', () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const teleportHtml = ctx.teleports!['#teleport3']
|
const teleportHtml = ctx.teleports!['#teleport3']
|
||||||
expect(teleportHtml).toMatchInlineSnapshot(`"<!--teleport anchor-->"`)
|
expect(teleportHtml).toMatchInlineSnapshot(
|
||||||
|
`"<!--teleport start anchor--><!--teleport anchor-->"`,
|
||||||
|
)
|
||||||
|
|
||||||
teleportContainer.innerHTML = teleportHtml
|
teleportContainer.innerHTML = teleportHtml
|
||||||
document.body.appendChild(teleportContainer)
|
document.body.appendChild(teleportContainer)
|
||||||
|
@ -413,7 +418,8 @@ describe('SSR hydration', () => {
|
||||||
expect(children[2].el).toBe(container.childNodes[6])
|
expect(children[2].el).toBe(container.childNodes[6])
|
||||||
|
|
||||||
expect(teleportVnode.target).toBe(teleportContainer)
|
expect(teleportVnode.target).toBe(teleportContainer)
|
||||||
expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[0])
|
expect(teleportVnode.targetStart).toBe(teleportContainer.childNodes[0])
|
||||||
|
expect(teleportVnode.targetAnchor).toBe(teleportContainer.childNodes[1])
|
||||||
|
|
||||||
// // event handler
|
// // event handler
|
||||||
triggerEvent('click', container.querySelector('.foo')!)
|
triggerEvent('click', container.querySelector('.foo')!)
|
||||||
|
@ -454,7 +460,7 @@ describe('SSR hydration', () => {
|
||||||
test('Teleport (as component root)', () => {
|
test('Teleport (as component root)', () => {
|
||||||
const teleportContainer = document.createElement('div')
|
const teleportContainer = document.createElement('div')
|
||||||
teleportContainer.id = 'teleport4'
|
teleportContainer.id = 'teleport4'
|
||||||
teleportContainer.innerHTML = `hello<!--teleport anchor-->`
|
teleportContainer.innerHTML = `<!--teleport start anchor-->hello<!--teleport anchor-->`
|
||||||
document.body.appendChild(teleportContainer)
|
document.body.appendChild(teleportContainer)
|
||||||
|
|
||||||
const wrapper = {
|
const wrapper = {
|
||||||
|
@ -483,7 +489,7 @@ describe('SSR hydration', () => {
|
||||||
test('Teleport (nested)', () => {
|
test('Teleport (nested)', () => {
|
||||||
const teleportContainer = document.createElement('div')
|
const teleportContainer = document.createElement('div')
|
||||||
teleportContainer.id = 'teleport5'
|
teleportContainer.id = 'teleport5'
|
||||||
teleportContainer.innerHTML = `<div><!--teleport start--><!--teleport end--></div><!--teleport anchor--><div>child</div><!--teleport anchor-->`
|
teleportContainer.innerHTML = `<!--teleport start anchor--><div><!--teleport start--><!--teleport end--></div><!--teleport anchor--><!--teleport start anchor--><div>child</div><!--teleport anchor-->`
|
||||||
document.body.appendChild(teleportContainer)
|
document.body.appendChild(teleportContainer)
|
||||||
|
|
||||||
const { vnode, container } = mountWithHydration(
|
const { vnode, container } = mountWithHydration(
|
||||||
|
@ -498,7 +504,7 @@ describe('SSR hydration', () => {
|
||||||
expect(vnode.anchor).toBe(container.lastChild)
|
expect(vnode.anchor).toBe(container.lastChild)
|
||||||
|
|
||||||
const childDivVNode = (vnode as any).children[0]
|
const childDivVNode = (vnode as any).children[0]
|
||||||
const div = teleportContainer.firstChild
|
const div = teleportContainer.childNodes[1]
|
||||||
expect(childDivVNode.el).toBe(div)
|
expect(childDivVNode.el).toBe(div)
|
||||||
expect(vnode.targetAnchor).toBe(div?.nextSibling)
|
expect(vnode.targetAnchor).toBe(div?.nextSibling)
|
||||||
|
|
||||||
|
@ -548,6 +554,66 @@ describe('SSR hydration', () => {
|
||||||
teleportContainer.id = 'target'
|
teleportContainer.id = 'target'
|
||||||
document.body.appendChild(teleportContainer)
|
document.body.appendChild(teleportContainer)
|
||||||
|
|
||||||
|
// server render
|
||||||
|
const ctx: SSRContext = {}
|
||||||
|
container.innerHTML = await renderToString(h(App), ctx)
|
||||||
|
expect(container.innerHTML).toBe(
|
||||||
|
'<div><!--teleport start--><!--teleport end--></div>',
|
||||||
|
)
|
||||||
|
teleportContainer.innerHTML = ctx.teleports!['#target']
|
||||||
|
|
||||||
|
// hydrate
|
||||||
|
createSSRApp(App).mount(container)
|
||||||
|
expect(container.innerHTML).toBe(
|
||||||
|
'<div><!--teleport start--><!--teleport end--></div>',
|
||||||
|
)
|
||||||
|
expect(teleportContainer.innerHTML).toBe(
|
||||||
|
'<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
|
||||||
|
)
|
||||||
|
expect(`Hydration children mismatch`).not.toHaveBeenWarned()
|
||||||
|
|
||||||
|
toggle.value = false
|
||||||
|
await nextTick()
|
||||||
|
expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
|
||||||
|
expect(teleportContainer.innerHTML).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Teleport unmount (mismatch + 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
|
// server render
|
||||||
container.innerHTML = await renderToString(h(App))
|
container.innerHTML = await renderToString(h(App))
|
||||||
expect(container.innerHTML).toBe(
|
expect(container.innerHTML).toBe(
|
||||||
|
@ -569,7 +635,7 @@ describe('SSR hydration', () => {
|
||||||
expect(teleportContainer.innerHTML).toBe('')
|
expect(teleportContainer.innerHTML).toBe('')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Teleport target change (full integration)', async () => {
|
test('Teleport target change (mismatch + full integration)', async () => {
|
||||||
const target = ref('#target1')
|
const target = ref('#target1')
|
||||||
const Comp = {
|
const Comp = {
|
||||||
template: `
|
template: `
|
||||||
|
|
|
@ -381,7 +381,8 @@ function hydrateTeleport(
|
||||||
slotScopeIds,
|
slotScopeIds,
|
||||||
optimized,
|
optimized,
|
||||||
)
|
)
|
||||||
vnode.targetStart = vnode.targetAnchor = targetNode
|
vnode.targetStart = targetNode
|
||||||
|
vnode.targetAnchor = targetNode && nextSibling(targetNode)
|
||||||
} else {
|
} else {
|
||||||
vnode.anchor = nextSibling(node)
|
vnode.anchor = nextSibling(node)
|
||||||
|
|
||||||
|
@ -390,28 +391,29 @@ function hydrateTeleport(
|
||||||
// could be nested teleports
|
// could be nested teleports
|
||||||
let targetAnchor = targetNode
|
let targetAnchor = targetNode
|
||||||
while (targetAnchor) {
|
while (targetAnchor) {
|
||||||
targetAnchor = nextSibling(targetAnchor)
|
if (targetAnchor && targetAnchor.nodeType === 8) {
|
||||||
if (
|
if ((targetAnchor as Comment).data === 'teleport start anchor') {
|
||||||
targetAnchor &&
|
vnode.targetStart = targetAnchor
|
||||||
targetAnchor.nodeType === 8 &&
|
} else if ((targetAnchor as Comment).data === 'teleport anchor') {
|
||||||
(targetAnchor as Comment).data === 'teleport anchor'
|
vnode.targetAnchor = targetAnchor
|
||||||
) {
|
;(target as TeleportTargetElement)._lpa =
|
||||||
vnode.targetAnchor = targetAnchor
|
vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
|
||||||
;(target as TeleportTargetElement)._lpa =
|
break
|
||||||
vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
|
}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
targetAnchor = nextSibling(targetAnchor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// #11400 if the HTML corresponding to Teleport is not embedded in the correct position
|
// #11400 if the HTML corresponding to Teleport is not embedded in the
|
||||||
// on the final page during SSR. the targetAnchor will always be null, we need to
|
// correct position on the final page during SSR. the targetAnchor will
|
||||||
// manually add targetAnchor to ensure Teleport it can properly unmount or move
|
// always be null, we need to manually add targetAnchor to ensure
|
||||||
|
// Teleport it can properly unmount or move
|
||||||
if (!vnode.targetAnchor) {
|
if (!vnode.targetAnchor) {
|
||||||
prepareAnchor(target, vnode, createText, insert)
|
prepareAnchor(target, vnode, createText, insert)
|
||||||
}
|
}
|
||||||
|
|
||||||
hydrateChildren(
|
hydrateChildren(
|
||||||
targetNode,
|
targetNode && nextSibling(targetNode),
|
||||||
vnode,
|
vnode,
|
||||||
target,
|
target,
|
||||||
parentComponent,
|
parentComponent,
|
||||||
|
|
|
@ -28,7 +28,7 @@ describe('ssrRenderTeleport', () => {
|
||||||
)
|
)
|
||||||
expect(html).toBe('<!--teleport start--><!--teleport end-->')
|
expect(html).toBe('<!--teleport start--><!--teleport end-->')
|
||||||
expect(ctx.teleports!['#target']).toBe(
|
expect(ctx.teleports!['#target']).toBe(
|
||||||
`<div>content</div><!--teleport anchor-->`,
|
`<!--teleport start anchor--><div>content</div><!--teleport anchor-->`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -56,7 +56,9 @@ describe('ssrRenderTeleport', () => {
|
||||||
expect(html).toBe(
|
expect(html).toBe(
|
||||||
'<!--teleport start--><div>content</div><!--teleport end-->',
|
'<!--teleport start--><div>content</div><!--teleport end-->',
|
||||||
)
|
)
|
||||||
expect(ctx.teleports!['#target']).toBe(`<!--teleport anchor-->`)
|
expect(ctx.teleports!['#target']).toBe(
|
||||||
|
`<!--teleport start anchor--><!--teleport anchor-->`,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('teleport rendering (vnode)', async () => {
|
test('teleport rendering (vnode)', async () => {
|
||||||
|
@ -73,7 +75,7 @@ describe('ssrRenderTeleport', () => {
|
||||||
)
|
)
|
||||||
expect(html).toBe('<!--teleport start--><!--teleport end-->')
|
expect(html).toBe('<!--teleport start--><!--teleport end-->')
|
||||||
expect(ctx.teleports!['#target']).toBe(
|
expect(ctx.teleports!['#target']).toBe(
|
||||||
'<span>hello</span><!--teleport anchor-->',
|
'<!--teleport start anchor--><span>hello</span><!--teleport anchor-->',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -93,7 +95,9 @@ describe('ssrRenderTeleport', () => {
|
||||||
expect(html).toBe(
|
expect(html).toBe(
|
||||||
'<!--teleport start--><span>hello</span><!--teleport end-->',
|
'<!--teleport start--><span>hello</span><!--teleport end-->',
|
||||||
)
|
)
|
||||||
expect(ctx.teleports!['#target']).toBe(`<!--teleport anchor-->`)
|
expect(ctx.teleports!['#target']).toBe(
|
||||||
|
`<!--teleport start anchor--><!--teleport anchor-->`,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('multiple teleports with same target', async () => {
|
test('multiple teleports with same target', async () => {
|
||||||
|
@ -115,7 +119,8 @@ describe('ssrRenderTeleport', () => {
|
||||||
'<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>',
|
'<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>',
|
||||||
)
|
)
|
||||||
expect(ctx.teleports!['#target']).toBe(
|
expect(ctx.teleports!['#target']).toBe(
|
||||||
'<span>hello</span><!--teleport anchor-->world<!--teleport anchor-->',
|
'<!--teleport start anchor--><span>hello</span><!--teleport anchor-->' +
|
||||||
|
'<!--teleport start anchor-->world<!--teleport anchor-->',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -134,7 +139,7 @@ describe('ssrRenderTeleport', () => {
|
||||||
)
|
)
|
||||||
expect(html).toBe('<!--teleport start--><!--teleport end-->')
|
expect(html).toBe('<!--teleport start--><!--teleport end-->')
|
||||||
expect(ctx.teleports!['#target']).toBe(
|
expect(ctx.teleports!['#target']).toBe(
|
||||||
`<div>content</div><!--teleport anchor-->`,
|
`<!--teleport start anchor--><div>content</div><!--teleport anchor-->`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -169,7 +174,7 @@ describe('ssrRenderTeleport', () => {
|
||||||
await p
|
await p
|
||||||
expect(html).toBe('<!--teleport start--><!--teleport end-->')
|
expect(html).toBe('<!--teleport start--><!--teleport end-->')
|
||||||
expect(ctx.teleports!['#target']).toBe(
|
expect(ctx.teleports!['#target']).toBe(
|
||||||
`<div>content</div><!--teleport anchor-->`,
|
`<!--teleport start anchor--><div>content</div><!--teleport anchor-->`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -29,9 +29,10 @@ export function ssrRenderTeleport(
|
||||||
|
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
contentRenderFn(parentPush)
|
contentRenderFn(parentPush)
|
||||||
teleportContent = `<!--teleport anchor-->`
|
teleportContent = `<!--teleport start anchor--><!--teleport anchor-->`
|
||||||
} else {
|
} else {
|
||||||
const { getBuffer, push } = createBuffer()
|
const { getBuffer, push } = createBuffer()
|
||||||
|
push(`<!--teleport start anchor-->`)
|
||||||
contentRenderFn(push)
|
contentRenderFn(push)
|
||||||
push(`<!--teleport anchor-->`)
|
push(`<!--teleport anchor-->`)
|
||||||
teleportContent = getBuffer()
|
teleportContent = getBuffer()
|
||||||
|
|
Loading…
Reference in New Issue