Skip to content

Commit

Permalink
fix(teleport/ssr): fix Teleport hydration regression due to targetSta…
Browse files Browse the repository at this point in the history
…rt anchor addition
  • Loading branch information
yyx990803 committed Jul 31, 2024
1 parent 12667da commit 7b18cdb
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 40 deletions.
100 changes: 83 additions & 17 deletions packages/runtime-core/__tests__/hydration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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],
teleportContainer.childNodes[1],
)
expect((vnode.children as VNode[])[1].el).toBe(
teleportContainer.childNodes[1],
teleportContainer.childNodes[2],
)
expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[2])
expect(vnode.targetAnchor).toBe(teleportContainer.childNodes[3])

// event handler
triggerEvent('click', teleportContainer.querySelector('.foo')!)
Expand All @@ -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-->`,
)
})

Expand Down Expand Up @@ -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
Expand All @@ -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')!)
Expand All @@ -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-->"`,
)
})

Expand All @@ -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)
Expand All @@ -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')!)
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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(
Expand All @@ -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)

Expand Down Expand Up @@ -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(
Expand All @@ -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: `
Expand Down
32 changes: 17 additions & 15 deletions packages/runtime-core/src/components/Teleport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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,
Expand Down
19 changes: 12 additions & 7 deletions packages/server-renderer/__tests__/ssrTeleport.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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-->`,
)
})

Expand Down Expand Up @@ -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 () => {
Expand All @@ -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-->',
)
})

Expand All @@ -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 () => {
Expand All @@ -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-->',
)
})

Expand All @@ -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-->`,
)
})

Expand Down Expand Up @@ -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-->`,
)
})
})
3 changes: 2 additions & 1 deletion packages/server-renderer/src/helpers/ssrRenderTeleport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 7b18cdb

Please sign in to comment.