Skip to content

Commit

Permalink
fix(ssr/teleport): support nested teleports in ssr
Browse files Browse the repository at this point in the history
fix #5242
  • Loading branch information
yyx990803 committed May 18, 2022
1 parent 84f0353 commit 595263c
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 29 deletions.
44 changes: 38 additions & 6 deletions packages/runtime-core/__tests__/hydration.spec.ts
Expand Up @@ -202,7 +202,7 @@ describe('SSR hydration', () => {
const fn = jest.fn()
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport'
teleportContainer.innerHTML = `<span>foo</span><span class="foo"></span><!---->`
teleportContainer.innerHTML = `<span>foo</span><span class="foo"></span><!--teleport anchor-->`
document.body.appendChild(teleportContainer)

const { vnode, container } = mountWithHydration(
Expand Down Expand Up @@ -233,7 +233,7 @@ describe('SSR hydration', () => {
msg.value = 'bar'
await nextTick()
expect(teleportContainer.innerHTML).toBe(
`<span>bar</span><span class="bar"></span><!---->`
`<span>bar</span><span class="bar"></span><!--teleport anchor-->`
)
})

Expand Down Expand Up @@ -263,7 +263,7 @@ describe('SSR hydration', () => {

const teleportHtml = ctx.teleports!['#teleport2']
expect(teleportHtml).toMatchInlineSnapshot(
`"<span>foo</span><span class=\\"foo\\"></span><!----><span>foo2</span><span class=\\"foo2\\"></span><!---->"`
`"<span>foo</span><span class=\\"foo\\"></span><!--teleport anchor--><span>foo2</span><span class=\\"foo2\\"></span><!--teleport anchor-->"`
)

teleportContainer.innerHTML = teleportHtml
Expand Down Expand Up @@ -300,7 +300,7 @@ describe('SSR hydration', () => {
msg.value = 'bar'
await nextTick()
expect(teleportContainer.innerHTML).toMatchInlineSnapshot(
`"<span>bar</span><span class=\\"bar\\"></span><!----><span>bar2</span><span class=\\"bar2\\"></span><!---->"`
`"<span>bar</span><span class=\\"bar\\"></span><!--teleport anchor--><span>bar2</span><span class=\\"bar2\\"></span><!--teleport anchor-->"`
)
})

Expand All @@ -327,7 +327,7 @@ describe('SSR hydration', () => {
)

const teleportHtml = ctx.teleports!['#teleport3']
expect(teleportHtml).toMatchInlineSnapshot(`"<!---->"`)
expect(teleportHtml).toMatchInlineSnapshot(`"<!--teleport anchor-->"`)

teleportContainer.innerHTML = teleportHtml
document.body.appendChild(teleportContainer)
Expand Down Expand Up @@ -369,7 +369,7 @@ describe('SSR hydration', () => {
test('Teleport (as component root)', () => {
const teleportContainer = document.createElement('div')
teleportContainer.id = 'teleport4'
teleportContainer.innerHTML = `hello<!---->`
teleportContainer.innerHTML = `hello<!--teleport anchor-->`
document.body.appendChild(teleportContainer)

const wrapper = {
Expand All @@ -395,6 +395,38 @@ describe('SSR hydration', () => {
expect(nextVNode.el).toBe(container.firstChild?.lastChild)
})

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-->`
document.body.appendChild(teleportContainer)

const { vnode, container } = mountWithHydration(
'<!--teleport start--><!--teleport end-->',
() =>
h(Teleport, { to: '#teleport5' }, [
h('div', [h(Teleport, { to: '#teleport5' }, [h('div', 'child')])])
])
)

expect(vnode.el).toBe(container.firstChild)
expect(vnode.anchor).toBe(container.lastChild)

const childDivVNode = (vnode as any).children[0]
const div = teleportContainer.firstChild
expect(childDivVNode.el).toBe(div)
expect(vnode.targetAnchor).toBe(div?.nextSibling)

const childTeleportVNode = childDivVNode.children[0]
expect(childTeleportVNode.el).toBe(div?.firstChild)
expect(childTeleportVNode.anchor).toBe(div?.lastChild)

expect(childTeleportVNode.targetAnchor).toBe(teleportContainer.lastChild)
expect(childTeleportVNode.children[0].el).toBe(
teleportContainer.lastChild?.previousSibling
)
})

// compile SSR + client render fn from the same template & hydrate
test('full compiler integration', async () => {
const mounted: string[] = []
Expand Down
23 changes: 20 additions & 3 deletions packages/runtime-core/src/components/Teleport.ts
Expand Up @@ -353,7 +353,26 @@ function hydrateTeleport(
vnode.targetAnchor = targetNode
} else {
vnode.anchor = nextSibling(node)
vnode.targetAnchor = hydrateChildren(

// lookahead until we find the target anchor
// we cannot rely on return value of hydrateChildren() because there
// 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
}
}

hydrateChildren(
targetNode,
vnode,
target,
Expand All @@ -363,8 +382,6 @@ function hydrateTeleport(
optimized
)
}
;(target as TeleportTargetElement)._lpa =
vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
}
}
return vnode.anchor && nextSibling(vnode.anchor as Node)
Expand Down
22 changes: 15 additions & 7 deletions packages/server-renderer/__tests__/ssrTeleport.spec.ts
Expand Up @@ -31,7 +31,9 @@ describe('ssrRenderTeleport', () => {
ctx
)
expect(html).toBe('<!--teleport start--><!--teleport end-->')
expect(ctx.teleports!['#target']).toBe(`<div>content</div><!---->`)
expect(ctx.teleports!['#target']).toBe(
`<div>content</div><!--teleport anchor-->`
)
})

test('teleport rendering (compiled + disabled)', async () => {
Expand All @@ -58,7 +60,7 @@ describe('ssrRenderTeleport', () => {
expect(html).toBe(
'<!--teleport start--><div>content</div><!--teleport end-->'
)
expect(ctx.teleports!['#target']).toBe(`<!---->`)
expect(ctx.teleports!['#target']).toBe(`<!--teleport anchor-->`)
})

test('teleport rendering (vnode)', async () => {
Expand All @@ -74,7 +76,9 @@ describe('ssrRenderTeleport', () => {
ctx
)
expect(html).toBe('<!--teleport start--><!--teleport end-->')
expect(ctx.teleports!['#target']).toBe('<span>hello</span><!---->')
expect(ctx.teleports!['#target']).toBe(
'<span>hello</span><!--teleport anchor-->'
)
})

test('teleport rendering (vnode + disabled)', async () => {
Expand All @@ -93,7 +97,7 @@ describe('ssrRenderTeleport', () => {
expect(html).toBe(
'<!--teleport start--><span>hello</span><!--teleport end-->'
)
expect(ctx.teleports!['#target']).toBe(`<!---->`)
expect(ctx.teleports!['#target']).toBe(`<!--teleport anchor-->`)
})

test('multiple teleports with same target', async () => {
Expand All @@ -115,7 +119,7 @@ describe('ssrRenderTeleport', () => {
'<div><!--teleport start--><!--teleport end--><!--teleport start--><!--teleport end--></div>'
)
expect(ctx.teleports!['#target']).toBe(
'<span>hello</span><!---->world<!---->'
'<span>hello</span><!--teleport anchor-->world<!--teleport anchor-->'
)
})

Expand All @@ -133,7 +137,9 @@ describe('ssrRenderTeleport', () => {
ctx
)
expect(html).toBe('<!--teleport start--><!--teleport end-->')
expect(ctx.teleports!['#target']).toBe(`<div>content</div><!---->`)
expect(ctx.teleports!['#target']).toBe(
`<div>content</div><!--teleport anchor-->`
)
})

test('teleport inside async component (stream)', async () => {
Expand Down Expand Up @@ -166,6 +172,8 @@ describe('ssrRenderTeleport', () => {
)
await p
expect(html).toBe('<!--teleport start--><!--teleport end-->')
expect(ctx.teleports!['#target']).toBe(`<div>content</div><!---->`)
expect(ctx.teleports!['#target']).toBe(
`<div>content</div><!--teleport anchor-->`
)
})
})
26 changes: 13 additions & 13 deletions packages/server-renderer/src/helpers/ssrRenderTeleport.ts
Expand Up @@ -10,28 +10,28 @@ export function ssrRenderTeleport(
) {
parentPush('<!--teleport start-->')

const context = parentComponent.appContext.provides[
ssrContextKey as any
] as SSRContext
const teleportBuffers =
context.__teleportBuffers || (context.__teleportBuffers = {})
const targetBuffer = teleportBuffers[target] || (teleportBuffers[target] = [])
// record current index of the target buffer to handle nested teleports
// since the parent needs to be rendered before the child
const bufferIndex = targetBuffer.length

let teleportContent: SSRBufferItem

if (disabled) {
contentRenderFn(parentPush)
teleportContent = `<!---->`
teleportContent = `<!--teleport anchor-->`
} else {
const { getBuffer, push } = createBuffer()
contentRenderFn(push)
push(`<!---->`) // teleport end anchor
push(`<!--teleport anchor-->`)
teleportContent = getBuffer()
}

const context = parentComponent.appContext.provides[
ssrContextKey as any
] as SSRContext
const teleportBuffers =
context.__teleportBuffers || (context.__teleportBuffers = {})
if (teleportBuffers[target]) {
teleportBuffers[target].push(teleportContent)
} else {
teleportBuffers[target] = [teleportContent]
}

targetBuffer.splice(bufferIndex, 0, teleportContent)
parentPush('<!--teleport end-->')
}

0 comments on commit 595263c

Please sign in to comment.