Explorar o código

fix(runtime-vapor): fix teleport unmount through vdom interop (#14720)

edison hai 1 semana
pai
achega
65379bd6dc

+ 74 - 0
packages/runtime-vapor/__tests__/components/Teleport.spec.ts

@@ -21,10 +21,12 @@ import {
   template,
   useVaporCssVars,
   vaporInteropPlugin,
+  withVaporCtx,
   withVaporDirectives,
 } from '@vue/runtime-vapor'
 import { makeRender } from '../_utils'
 import {
+  defineComponent,
   h,
   nextTick,
   onActivated,
@@ -34,6 +36,7 @@ import {
   onUnmounted,
   reactive,
   ref,
+  renderSlot,
   shallowRef,
 } from 'vue'
 
@@ -1354,6 +1357,77 @@ function runSharedTests(deferMode: boolean): void {
     expect(target.innerHTML).toBe('')
   })
 
+  test('should unmount teleport nested under vdom components when toggled off', async () => {
+    const root = document.createElement('div')
+    document.body.appendChild(root)
+    const show = ref(true)
+
+    const Comp1 = defineComponent({
+      setup(_, { slots }) {
+        return () => renderSlot(slots, 'default')
+      },
+    })
+
+    const Comp2 = defineComponent({
+      setup(_, { slots }) {
+        return () => renderSlot(slots, 'default')
+      },
+    })
+
+    const App = defineVaporComponent({
+      setup() {
+        const n0 = template('<button></button>')()
+        const n1 = createIf(
+          () => show.value,
+          () =>
+            createComponent(Comp1 as any, null, {
+              default: withVaporCtx(() =>
+                createComponent(Comp2 as any, null, {
+                  default: withVaporCtx(() =>
+                    createComponent(
+                      VaporTeleport,
+                      {
+                        to: () => 'body',
+                      },
+                      {
+                        default: () => template('<input>')(),
+                      },
+                    ),
+                  ),
+                }),
+              ),
+            }),
+        )
+        return [n0, n1]
+      },
+    })
+
+    const app = createVaporApp(App)
+    app.use(vaporInteropPlugin)
+    try {
+      app.mount(root)
+
+      expect(document.body.querySelectorAll('input')).toHaveLength(1)
+
+      show.value = false
+      await nextTick()
+
+      expect(root.innerHTML).toBe('<button></button><!--if-->')
+      expect(document.body.querySelectorAll('input')).toHaveLength(0)
+
+      show.value = true
+      await nextTick()
+
+      expect(root.innerHTML).toBe(
+        '<button></button><!--teleport start--><!--teleport end--><!--if-->',
+      )
+      expect(document.body.querySelectorAll('input')).toHaveLength(1)
+    } finally {
+      app.unmount()
+      root.remove()
+    }
+  })
+
   test('unmount previous sibling node inside target node', async () => {
     const root = document.createElement('div')
     const parentShow = ref(false)

+ 5 - 1
packages/runtime-vapor/src/component.ts

@@ -331,7 +331,11 @@ export function createComponent(
     // teleport
     if (isVaporTeleport(component)) {
       const frag = component.process(rawProps!, rawSlots!)
-      onScopeDispose(() => remove(frag), true)
+      if (_insertionParent) {
+        // Teleports mounted via insertion state are not part of the returned
+        // block tree, so scope disposal must tear down their target-side state.
+        onScopeDispose(() => frag.dispose(), true)
+      }
       if (!isHydrating) {
         if (_insertionParent) insert(frag, _insertionParent, _insertionAnchor)
       } else {

+ 10 - 5
packages/runtime-vapor/src/components/Teleport.ts

@@ -339,7 +339,7 @@ export class TeleportFragment extends VaporFragment {
     this.handlePropsUpdate()
   }
 
-  remove = (parent: ParentNode | undefined = this.parent!): void => {
+  dispose = (): void => {
     if (this.mountToTargetJob) {
       this.mountToTargetJob.flags! |= SchedulerJobFlags.DISPOSED
       this.mountToTargetJob = undefined
@@ -363,18 +363,23 @@ export class TeleportFragment extends VaporFragment {
       this.targetAnchor = undefined
     }
 
+    this.target = undefined
+    this.mountContainer = undefined
+    this.mountAnchor = undefined
+  }
+
+  remove = (_parent?: ParentNode): void => {
+    this.dispose()
+
     if (this.anchor) {
       remove(this.anchor, parentNode(this.anchor)!)
       this.anchor = undefined
     }
 
     if (this.placeholder) {
-      remove(this.placeholder!, parent)
+      remove(this.placeholder!, parentNode(this.placeholder) as ParentNode)
       this.placeholder = undefined
     }
-
-    this.mountContainer = undefined
-    this.mountAnchor = undefined
   }
 
   private hydrateTargetAnchors(

+ 10 - 1
packages/runtime-vapor/src/vdomInterop.ts

@@ -278,7 +278,16 @@ const vaporInteropImpl: Omit<
       remove(vnode.vb, container)
       stopVaporSlotScope(vnode)
     }
-    remove(vnode.anchor as Node, container)
+    if (doRemove) {
+      const anchor = vnode.anchor as Node
+      // `container` is captured before unmount starts, but the unmount above
+      // may already remove or move this anchor. Only remove it if it is still
+      // attached, using its current parent instead of the stale snapshot.
+      const parent = anchor.parentNode
+      if (parent) {
+        remove(anchor, parent)
+      }
+    }
   },
 
   /**