]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(ssr/teleport): support nested teleports in ssr
authorEvan You <yyx990803@gmail.com>
Wed, 18 May 2022 10:13:08 +0000 (18:13 +0800)
committerEvan You <yyx990803@gmail.com>
Wed, 18 May 2022 10:13:08 +0000 (18:13 +0800)
fix #5242

packages/runtime-core/__tests__/hydration.spec.ts
packages/runtime-core/src/components/Teleport.ts
packages/server-renderer/__tests__/ssrTeleport.spec.ts
packages/server-renderer/src/helpers/ssrRenderTeleport.ts

index 76e8b15342f589b8968ec03a78a1457f2c7d1a39..58661233564c81f56d7861db8ea247096f465879 100644 (file)
@@ -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(
@@ -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-->`
     )
   })
 
@@ -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
@@ -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-->"`
     )
   })
 
@@ -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)
@@ -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 = {
@@ -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[] = []
index 68d50a63fbdb57ed6aac85864c7de11a02e43f2d..06b69aff4ec911e2db330b73de20eaa2c9e60be1 100644 (file)
@@ -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,
@@ -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)
index af915168b3c5f3b3202f6231e5f365a2d93d313d..76c5c8eb3d91f08031bc0eec508a594569abbc62 100644 (file)
@@ -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 () => {
@@ -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 () => {
@@ -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 () => {
@@ -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 () => {
@@ -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-->'
     )
   })
 
@@ -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 () => {
@@ -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-->`
+    )
   })
 })
index 77331b7bddddf3a47725d5bda96c825f59647d28..8338ec06c25f1a5f211580a44c245d23107d1e47 100644 (file)
@@ -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-->')
 }