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 teleportContainer = document.createElement('div')
|
||||
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)
|
||||
|
||||
const { vnode, container } = mountWithHydration(
|
||||
|
@ -281,13 +281,14 @@ describe('SSR hydration', () => {
|
|||
expect(vnode.anchor).toBe(container.lastChild)
|
||||
|
||||
expect(vnode.target).toBe(teleportContainer)
|
||||
expect(vnode.targetStart).toBe(teleportContainer.childNodes[0])
|
||||
expect((vnode.children as VNode[])[0].el).toBe(
|
||||
teleportContainer.childNodes[0],
|
||||
)
|
||||
expect((vnode.children as VNode[])[1].el).toBe(
|
||||
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
|
||||
triggerEvent('click', teleportContainer.querySelector('.foo')!)
|
||||
|
@ -296,7 +297,7 @@ describe('SSR hydration', () => {
|
|||
msg.value = 'bar'
|
||||
await nextTick()
|
||||
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']
|
||||
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
|
||||
|
@ -342,16 +343,18 @@ describe('SSR hydration', () => {
|
|||
expect(teleportVnode2.anchor).toBe(container.childNodes[4])
|
||||
|
||||
expect(teleportVnode1.target).toBe(teleportContainer)
|
||||
expect(teleportVnode1.targetStart).toBe(teleportContainer.childNodes[0])
|
||||
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.targetStart).toBe(teleportContainer.childNodes[4])
|
||||
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
|
||||
triggerEvent('click', teleportContainer.querySelector('.foo')!)
|
||||
|
@ -363,7 +366,7 @@ describe('SSR hydration', () => {
|
|||
msg.value = 'bar'
|
||||
await nextTick()
|
||||
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']
|
||||
expect(teleportHtml).toMatchInlineSnapshot(`"<!--teleport anchor-->"`)
|
||||
expect(teleportHtml).toMatchInlineSnapshot(
|
||||
`"<!--teleport start anchor--><!--teleport anchor-->"`,
|
||||
)
|
||||
|
||||
teleportContainer.innerHTML = teleportHtml
|
||||
document.body.appendChild(teleportContainer)
|
||||
|
@ -413,7 +418,8 @@ describe('SSR hydration', () => {
|
|||
expect(children[2].el).toBe(container.childNodes[6])
|
||||
|
||||
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
|
||||
triggerEvent('click', container.querySelector('.foo')!)
|
||||
|
@ -454,7 +460,7 @@ describe('SSR hydration', () => {
|
|||
test('Teleport (as component root)', () => {
|
||||
const teleportContainer = document.createElement('div')
|
||||
teleportContainer.id = 'teleport4'
|
||||
teleportContainer.innerHTML = `hello<!--teleport anchor-->`
|
||||
teleportContainer.innerHTML = `<!--teleport start anchor-->hello<!--teleport anchor-->`
|
||||
document.body.appendChild(teleportContainer)
|
||||
|
||||
const wrapper = {
|
||||
|
@ -483,7 +489,7 @@ describe('SSR hydration', () => {
|
|||
test('Teleport (nested)', () => {
|
||||
const teleportContainer = document.createElement('div')
|
||||
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)
|
||||
|
||||
const { vnode, container } = mountWithHydration(
|
||||
|
@ -498,7 +504,7 @@ describe('SSR hydration', () => {
|
|||
expect(vnode.anchor).toBe(container.lastChild)
|
||||
|
||||
const childDivVNode = (vnode as any).children[0]
|
||||
const div = teleportContainer.firstChild
|
||||
const div = teleportContainer.childNodes[1]
|
||||
expect(childDivVNode.el).toBe(div)
|
||||
expect(vnode.targetAnchor).toBe(div?.nextSibling)
|
||||
|
||||
|
@ -548,6 +554,66 @@ describe('SSR hydration', () => {
|
|||
teleportContainer.id = 'target'
|
||||
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
|
||||
container.innerHTML = await renderToString(h(App))
|
||||
expect(container.innerHTML).toBe(
|
||||
|
@ -569,7 +635,7 @@ describe('SSR hydration', () => {
|
|||
expect(teleportContainer.innerHTML).toBe('')
|
||||
})
|
||||
|
||||
test('Teleport target change (full integration)', async () => {
|
||||
test('Teleport target change (mismatch + full integration)', async () => {
|
||||
const target = ref('#target1')
|
||||
const Comp = {
|
||||
template: `
|
||||
|
|
|
@ -381,7 +381,8 @@ function hydrateTeleport(
|
|||
slotScopeIds,
|
||||
optimized,
|
||||
)
|
||||
vnode.targetStart = vnode.targetAnchor = targetNode
|
||||
vnode.targetStart = targetNode
|
||||
vnode.targetAnchor = targetNode && nextSibling(targetNode)
|
||||
} else {
|
||||
vnode.anchor = nextSibling(node)
|
||||
|
||||
|
@ -390,28 +391,29 @@ function hydrateTeleport(
|
|||
// could be nested teleports
|
||||
let targetAnchor = targetNode
|
||||
while (targetAnchor) {
|
||||
targetAnchor = nextSibling(targetAnchor)
|
||||
if (
|
||||
targetAnchor &&
|
||||
targetAnchor.nodeType === 8 &&
|
||||
(targetAnchor as Comment).data === 'teleport anchor'
|
||||
) {
|
||||
vnode.targetAnchor = targetAnchor
|
||||
;(target as TeleportTargetElement)._lpa =
|
||||
vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
|
||||
break
|
||||
if (targetAnchor && targetAnchor.nodeType === 8) {
|
||||
if ((targetAnchor as Comment).data === 'teleport start anchor') {
|
||||
vnode.targetStart = targetAnchor
|
||||
} else if ((targetAnchor as Comment).data === 'teleport anchor') {
|
||||
vnode.targetAnchor = targetAnchor
|
||||
;(target as TeleportTargetElement)._lpa =
|
||||
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
|
||||
// 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
|
||||
// #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,
|
||||
targetNode && nextSibling(targetNode),
|
||||
vnode,
|
||||
target,
|
||||
parentComponent,
|
||||
|
|
|
@ -28,7 +28,7 @@ describe('ssrRenderTeleport', () => {
|
|||
)
|
||||
expect(html).toBe('<!--teleport start--><!--teleport end-->')
|
||||
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(
|
||||
'<!--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 () => {
|
||||
|
@ -73,7 +75,7 @@ describe('ssrRenderTeleport', () => {
|
|||
)
|
||||
expect(html).toBe('<!--teleport start--><!--teleport end-->')
|
||||
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(
|
||||
'<!--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 () => {
|
||||
|
@ -115,7 +119,8 @@ describe('ssrRenderTeleport', () => {
|
|||
'<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>',
|
||||
)
|
||||
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(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
|
||||
expect(html).toBe('<!--teleport start--><!--teleport end-->')
|
||||
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) {
|
||||
contentRenderFn(parentPush)
|
||||
teleportContent = `<!--teleport anchor-->`
|
||||
teleportContent = `<!--teleport start anchor--><!--teleport anchor-->`
|
||||
} else {
|
||||
const { getBuffer, push } = createBuffer()
|
||||
push(`<!--teleport start anchor-->`)
|
||||
contentRenderFn(push)
|
||||
push(`<!--teleport anchor-->`)
|
||||
teleportContent = getBuffer()
|
||||
|
|
Loading…
Reference in New Issue