]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(teleport): skip teleported nodes when locating patch anchor
authorEvan You <evan@vuejs.org>
Tue, 16 Jul 2024 09:17:25 +0000 (17:17 +0800)
committerEvan You <evan@vuejs.org>
Tue, 16 Jul 2024 09:17:25 +0000 (17:17 +0800)
close #9071
close #9134
close #9313

Tests reused from #9313

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

index 9c85cd8beb656c94f6109072758ff5408d294fa1..aca9432b6e16dd7547fbb20143190a5db62af966 100644 (file)
@@ -16,7 +16,7 @@ import {
   serializeInner,
   withDirectives,
 } from '@vue/runtime-test'
-import { Fragment, createVNode } from '../../src/vnode'
+import { Fragment, createCommentVNode, createVNode } from '../../src/vnode'
 import { compile, render as domRender } from 'vue'
 
 describe('renderer: teleport', () => {
@@ -553,4 +553,71 @@ describe('renderer: teleport', () => {
       `"<div>teleported</div>"`,
     )
   })
+
+  //#9071
+  test('toggle sibling node inside target node', async () => {
+    const root = document.createElement('div')
+    const show = ref(false)
+    const App = defineComponent({
+      setup() {
+        return () => {
+          return show.value
+            ? h(Teleport, { to: root }, [h('div', 'teleported')])
+            : h('div', 'foo')
+        }
+      },
+    })
+
+    domRender(h(App), root)
+    expect(root.innerHTML).toMatchInlineSnapshot('"<div>foo</div>"')
+
+    show.value = true
+    await nextTick()
+
+    expect(root.innerHTML).toMatchInlineSnapshot(
+      '"<!--teleport start--><!--teleport end--><div>teleported</div>"',
+    )
+
+    show.value = false
+    await nextTick()
+
+    expect(root.innerHTML).toMatchInlineSnapshot('"<div>foo</div>"')
+  })
+
+  test('unmount previous sibling node inside target node', async () => {
+    const root = document.createElement('div')
+    const parentShow = ref(false)
+    const childShow = ref(true)
+
+    const Comp = {
+      setup() {
+        return () => h(Teleport, { to: root }, [h('div', 'foo')])
+      },
+    }
+
+    const App = defineComponent({
+      setup() {
+        return () => {
+          return parentShow.value
+            ? h(Fragment, { key: 0 }, [
+                childShow.value ? h(Comp) : createCommentVNode('v-if'),
+              ])
+            : createCommentVNode('v-if')
+        }
+      },
+    })
+
+    domRender(h(App), root)
+    expect(root.innerHTML).toMatchInlineSnapshot('"<!--v-if-->"')
+
+    parentShow.value = true
+    await nextTick()
+    expect(root.innerHTML).toMatchInlineSnapshot(
+      '"<!--teleport start--><!--teleport end--><div>foo</div>"',
+    )
+
+    parentShow.value = false
+    await nextTick()
+    expect(root.innerHTML).toMatchInlineSnapshot('"<!--v-if-->"')
+  })
 })
index a10ae84d428a8a372406980f26bf992f95a6ffc3..65437300cff7833d1f6082e9eaddc0ce1c6c75fe 100644 (file)
@@ -21,6 +21,8 @@ export interface TeleportProps {
   disabled?: boolean
 }
 
+export const TeleportEndKey = Symbol('_vte')
+
 export const isTeleport = (type: any): boolean => type.__isTeleport
 
 const isTeleportDisabled = (props: VNode['props']): boolean =>
@@ -105,11 +107,16 @@ export const TeleportImpl = {
       const mainAnchor = (n2.anchor = __DEV__
         ? createComment('teleport end')
         : createText(''))
-      insert(placeholder, container, anchor)
-      insert(mainAnchor, container, anchor)
       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
       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)) {
@@ -146,6 +153,7 @@ export const TeleportImpl = {
     } else {
       // update content
       n2.el = n1.el
+      n2.targetStart = n1.targetStart
       const mainAnchor = (n2.anchor = n1.anchor)!
       const target = (n2.target = n1.target)!
       const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
@@ -253,9 +261,18 @@ export const TeleportImpl = {
     { um: unmount, o: { remove: hostRemove } }: RendererInternals,
     doRemove: boolean,
   ) {
-    const { shapeFlag, children, anchor, targetAnchor, target, props } = vnode
+    const {
+      shapeFlag,
+      children,
+      anchor,
+      targetStart,
+      targetAnchor,
+      target,
+      props,
+    } = vnode
 
     if (target) {
+      hostRemove(targetStart!)
       hostRemove(targetAnchor!)
     }
 
index 8a64baad1c0768fc47fcc791f43cb314f5dfd768..088d1565cbb6bd80778203dd6e876c473820cf0e 100644 (file)
@@ -58,7 +58,11 @@ import {
   type SuspenseImpl,
   queueEffectWithSuspense,
 } from './components/Suspense'
-import type { TeleportImpl, TeleportVNode } from './components/Teleport'
+import {
+  TeleportEndKey,
+  type TeleportImpl,
+  type TeleportVNode,
+} from './components/Teleport'
 import { type KeepAliveContext, isKeepAlive } from './components/KeepAlive'
 import { isHmrUpdating, registerHMR, unregisterHMR } from './hmr'
 import { type RootHydrateFunction, createHydrationFunctions } from './hydration'
@@ -140,7 +144,7 @@ export interface RendererOptions<
 // functions provided via options, so the internal constraint is really just
 // a generic object.
 export interface RendererNode {
-  [key: string]: any
+  [key: string | symbol]: any
 }
 
 export interface RendererElement extends RendererNode {}
@@ -2368,7 +2372,12 @@ function baseCreateRenderer(
     if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
       return vnode.suspense!.next()
     }
-    return hostNextSibling((vnode.anchor || vnode.el)!)
+    const el = hostNextSibling((vnode.anchor || vnode.el)!)
+    // #9071, #9313
+    // teleported content can mess up nextSibling searches during patch so
+    // we need to skip them during nextSibling search
+    const teleportEnd = el && el[TeleportEndKey]
+    return teleportEnd ? hostNextSibling(teleportEnd) : el
   }
 
   let isFlushing = false
index a0d4074aaea3626aa7102923416fcefc694cfe8f..75024b73c77c8658e5d6d054376e082c2a5dbaa6 100644 (file)
@@ -198,6 +198,7 @@ export interface VNode<
   el: HostNode | null
   anchor: HostNode | null // fragment anchor
   target: HostElement | null // teleport target
+  targetStart: HostNode | null // teleport target start anchor
   targetAnchor: HostNode | null // teleport target anchor
   /**
    * number of elements contained in a static vnode
@@ -477,6 +478,7 @@ function createBaseVNode(
     el: null,
     anchor: null,
     target: null,
+    targetStart: null,
     targetAnchor: null,
     staticCount: 0,
     shapeFlag,
@@ -677,6 +679,7 @@ export function cloneVNode<T, U>(
         ? (children as VNode[]).map(deepCloneVNode)
         : children,
     target: vnode.target,
+    targetStart: vnode.targetStart,
     targetAnchor: vnode.targetAnchor,
     staticCount: vnode.staticCount,
     shapeFlag: vnode.shapeFlag,