]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(teleport): properly handling disabled teleport target anchor (#14417)
authoredison <daiwei521@126.com>
Mon, 9 Feb 2026 07:53:25 +0000 (15:53 +0800)
committerGitHub <noreply@github.com>
Mon, 9 Feb 2026 07:53:25 +0000 (15:53 +0800)
close #14412

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

index 7017c0414f43db08ed7f317d8a1c19366d227130..92b2edb7833d329c43a4ab581342c649e82ee3b3 100644 (file)
@@ -683,6 +683,77 @@ describe('SSR hydration', () => {
     expect(teleportContainer.innerHTML).toBe('')
   })
 
+  test('Teleport unmount (disabled + full integration)', async () => {
+    const disabled = ref(true)
+    const target = ref('#teleport001')
+    const toggle = ref(true)
+
+    const Comp = {
+      template: `
+      <div>
+        <div id="teleport001">
+          <Teleport
+            :to="target"
+            :disabled="disabled"
+          >
+            <template v-for="section in order">
+              <div>{{section}}</div>
+            </template>
+          </Teleport>
+        </div>
+        <div id="teleport002"></div>
+      </div>
+      `,
+      setup() {
+        const order = ref(['A', 'B', 'C'])
+        return { target, disabled, order }
+      },
+    }
+    const App = {
+      template: `<Comp v-if="toggle"/>`,
+      components: {
+        Comp,
+      },
+      setup() {
+        return { toggle }
+      },
+    }
+
+    const container = document.createElement('div')
+    document.body.appendChild(container)
+
+    // server render
+    container.innerHTML = await renderToString(h(App))
+    expect(container.innerHTML).toBe(
+      `<div>` +
+        `<div id="teleport001">` +
+        `<!--teleport start-->` +
+        `<!--[--><div>A</div><div>B</div><div>C</div><!--]-->` +
+        `<!--teleport end-->` +
+        `</div>` +
+        `<div id="teleport002"></div>` +
+        `</div>`,
+    )
+
+    // hydrate
+    createSSRApp(App).mount(container)
+    expect(`Hydration children mismatch`).not.toHaveBeenWarned()
+
+    target.value = '#teleport002'
+    disabled.value = false
+    await nextTick()
+    expect(container.querySelector('#teleport001')!.innerHTML).toBe(
+      '<!--teleport start--><!--teleport end-->',
+    )
+    expect(container.querySelector('#teleport002')!.innerHTML).toBe(
+      '<!--[--><div>A</div><div>B</div><div>C</div><!--]-->',
+    )
+
+    toggle.value = false
+    await nextTick()
+    expect(container.innerHTML).toBe('<!--v-if-->')
+  })
+
   test('Teleport target change (mismatch + full integration)', async () => {
     const target = ref('#target1')
     const Comp = {
index 4961d0fe0482828b2338d1ca7d4a4cec179e076f..bdf0c3fdd287ae43d9356f856f9294d50a1da5ab 100644 (file)
@@ -412,12 +412,30 @@ function hydrateTeleport(
     optimized: boolean,
   ) => Node | null,
 ): Node | null {
-  function hydrateDisabledTeleport(
-    node: Node,
-    vnode: VNode,
-    targetStart: Node | null,
-    targetAnchor: Node | null,
+  // lookahead until we find the target anchor
+  // we cannot rely on return value of hydrateChildren() because there
+  // could be nested teleports
+  function hydrateAnchor(
+    target: TeleportTargetElement,
+    targetNode: Node | null,
   ) {
+    let targetAnchor = targetNode
+    while (targetAnchor) {
+      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._lpa =
+            vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
+          break
+        }
+      }
+      targetAnchor = nextSibling(targetAnchor)
+    }
+  }
+
+  function hydrateDisabledTeleport(node: Node, vnode: VNode) {
     vnode.anchor = hydrateChildren(
       nextSibling(node),
       vnode,
@@ -427,8 +445,6 @@ function hydrateTeleport(
       slotScopeIds,
       optimized,
     )
-    vnode.targetStart = targetStart
-    vnode.targetAnchor = targetAnchor
   }
 
   const target = (vnode.target = resolveTarget<Element>(
@@ -443,33 +459,22 @@ function hydrateTeleport(
       (target as TeleportTargetElement)._lpa || target.firstChild
     if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
       if (disabled) {
-        hydrateDisabledTeleport(
-          node,
-          vnode,
-          targetNode,
-          targetNode && nextSibling(targetNode),
-        )
+        hydrateDisabledTeleport(node, vnode)
+        hydrateAnchor(target as TeleportTargetElement, targetNode)
+        if (!vnode.targetAnchor) {
+          prepareAnchor(
+            target,
+            vnode,
+            createText,
+            insert,
+            // if target is the same as the main view, insert anchors before current node
+            // to avoid hydrating mismatch
+            parentNode(node)! === target ? node : null,
+          )
+        }
       } else {
         vnode.anchor = nextSibling(node)
-
-        // 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) {
-          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)
-        }
-
+        hydrateAnchor(target as TeleportTargetElement, targetNode)
         // #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
@@ -492,7 +497,9 @@ function hydrateTeleport(
     updateCssVars(vnode, disabled)
   } else if (disabled) {
     if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
-      hydrateDisabledTeleport(node, vnode, node, nextSibling(node))
+      hydrateDisabledTeleport(node, vnode)
+      vnode.targetStart = node
+      vnode.targetAnchor = nextSibling(node)
     }
   }
   return vnode.anchor && nextSibling(vnode.anchor as Node)
@@ -535,6 +542,7 @@ function prepareAnchor(
   vnode: TeleportVNode,
   createText: RendererOptions['createText'],
   insert: RendererOptions['insert'],
+  anchor: RendererNode | null = null,
 ) {
   const targetStart = (vnode.targetStart = createText(''))
   const targetAnchor = (vnode.targetAnchor = createText(''))
@@ -544,8 +552,8 @@ function prepareAnchor(
   targetStart[TeleportEndKey] = targetAnchor
 
   if (target) {
-    insert(targetStart, target)
-    insert(targetAnchor, target)
+    insert(targetStart, target, anchor)
+    insert(targetAnchor, target, anchor)
   }
 
   return targetAnchor