]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(Teleport): ensure targetAnchor and targetStart not null during hydration (#11456)
authoredison <daiwei521@126.com>
Wed, 31 Jul 2024 07:46:39 +0000 (15:46 +0800)
committerGitHub <noreply@github.com>
Wed, 31 Jul 2024 07:46:39 +0000 (15:46 +0800)
close #11400

packages/runtime-core/__tests__/hydration.spec.ts
packages/runtime-core/src/components/Teleport.ts

index ea1d626f7c4ccfb41a1d20bb6c76899e3ced9753..9916fafa62c9bcedade78dea6ff76a1bc22c1b26 100644 (file)
@@ -512,6 +512,118 @@ describe('SSR hydration', () => {
     )
   })
 
+  test('Teleport unmount (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(
+      '<div><!--teleport start--><!--teleport end--></div>',
+    )
+    expect(teleportContainer.innerHTML).toBe('')
+
+    // hydrate
+    createSSRApp(App).mount(container)
+    expect(container.innerHTML).toBe(
+      '<div><!--teleport start--><!--teleport end--></div>',
+    )
+    expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>')
+    expect(`Hydration children mismatch`).toHaveBeenWarned()
+
+    toggle.value = false
+    await nextTick()
+    expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
+    expect(teleportContainer.innerHTML).toBe('')
+  })
+
+  test('Teleport target change (full integration)', async () => {
+    const target = ref('#target1')
+    const Comp = {
+      template: `
+        <Teleport :to="target"> 
+          <span>Teleported</span>
+        </Teleport>
+      `,
+      setup() {
+        return { target }
+      },
+    }
+
+    const App = {
+      template: `
+        <div>
+          <Comp />
+        </div>
+      `,
+      components: {
+        Comp,
+      },
+    }
+
+    const container = document.createElement('div')
+    const teleportContainer1 = document.createElement('div')
+    teleportContainer1.id = 'target1'
+    const teleportContainer2 = document.createElement('div')
+    teleportContainer2.id = 'target2'
+    document.body.appendChild(teleportContainer1)
+    document.body.appendChild(teleportContainer2)
+
+    // server render
+    container.innerHTML = await renderToString(h(App))
+    expect(container.innerHTML).toBe(
+      '<div><!--teleport start--><!--teleport end--></div>',
+    )
+    expect(teleportContainer1.innerHTML).toBe('')
+    expect(teleportContainer2.innerHTML).toBe('')
+
+    // hydrate
+    createSSRApp(App).mount(container)
+    expect(container.innerHTML).toBe(
+      '<div><!--teleport start--><!--teleport end--></div>',
+    )
+    expect(teleportContainer1.innerHTML).toBe('<span>Teleported</span>')
+    expect(teleportContainer2.innerHTML).toBe('')
+    expect(`Hydration children mismatch`).toHaveBeenWarned()
+
+    target.value = '#target2'
+    await nextTick()
+    expect(teleportContainer1.innerHTML).toBe('')
+    expect(teleportContainer2.innerHTML).toBe('<span>Teleported</span>')
+  })
+
   // compile SSR + client render fn from the same template & hydrate
   test('full compiler integration', async () => {
     const mounted: string[] = []
index 65437300cff7833d1f6082e9eaddc0ce1c6c75fe..81573cc85a74823885059c91474752153162a7f7 100644 (file)
@@ -107,17 +107,11 @@ export const TeleportImpl = {
       const mainAnchor = (n2.anchor = __DEV__
         ? createComment('teleport end')
         : createText(''))
-      const target = (n2.target = resolveTarget(n2.props, querySelector))
-      const targetStart = (n2.targetStart = createText(''))
-      const targetAnchor = (n2.targetAnchor = createText(''))
       insert(placeholder, container, anchor)
       insert(mainAnchor, container, anchor)
-      // attach a special property so we can skip teleported content in
-      // renderer's nextSibling search
-      targetStart[TeleportEndKey] = targetAnchor
+      const target = (n2.target = resolveTarget(n2.props, querySelector))
+      const targetAnchor = prepareAnchor(target, n2, createText, insert)
       if (target) {
-        insert(targetStart, target)
-        insert(targetAnchor, target)
         // #2652 we could be teleporting from a non-SVG tree into an SVG tree
         if (namespace === 'svg' || isTargetSVG(target)) {
           namespace = 'svg'
@@ -355,7 +349,7 @@ function hydrateTeleport(
   slotScopeIds: string[] | null,
   optimized: boolean,
   {
-    o: { nextSibling, parentNode, querySelector },
+    o: { nextSibling, parentNode, querySelector, insert, createText },
   }: RendererInternals<Node, Element>,
   hydrateChildren: (
     node: Node | null,
@@ -387,7 +381,7 @@ function hydrateTeleport(
           slotScopeIds,
           optimized,
         )
-        vnode.targetAnchor = targetNode
+        vnode.targetStart = vnode.targetAnchor = targetNode
       } else {
         vnode.anchor = nextSibling(node)
 
@@ -409,6 +403,13 @@ function hydrateTeleport(
           }
         }
 
+        // #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,
           vnode,
@@ -449,3 +450,24 @@ function updateCssVars(vnode: VNode) {
     ctx.ut()
   }
 }
+
+function prepareAnchor(
+  target: RendererElement | null,
+  vnode: TeleportVNode,
+  createText: RendererOptions['createText'],
+  insert: RendererOptions['insert'],
+) {
+  const targetStart = (vnode.targetStart = createText(''))
+  const targetAnchor = (vnode.targetAnchor = createText(''))
+
+  // attach a special property, so we can skip teleported content in
+  // renderer's nextSibling search
+  targetStart[TeleportEndKey] = targetAnchor
+
+  if (target) {
+    insert(targetStart, target)
+    insert(targetAnchor, target)
+  }
+
+  return targetAnchor
+}