]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: save
authordaiwei <daiwei521@126.com>
Fri, 21 Mar 2025 06:43:27 +0000 (14:43 +0800)
committerdaiwei <daiwei521@126.com>
Fri, 21 Mar 2025 06:43:27 +0000 (14:43 +0800)
packages/runtime-vapor/__tests__/components/Teleport.spec.ts [new file with mode: 0644]
packages/runtime-vapor/src/components/Teleport.ts

diff --git a/packages/runtime-vapor/__tests__/components/Teleport.spec.ts b/packages/runtime-vapor/__tests__/components/Teleport.spec.ts
new file mode 100644 (file)
index 0000000..373d170
--- /dev/null
@@ -0,0 +1,252 @@
+import {
+  type LooseRawProps,
+  type VaporComponent,
+  createComponent as originalCreateComponent,
+} from '../../src/component'
+import { VaporTeleport, template } from '@vue/runtime-vapor'
+
+import { makeRender } from '../_utils'
+import { nextTick, onBeforeUnmount, onUnmounted, ref, shallowRef } from 'vue'
+
+const define = makeRender()
+
+describe('renderer: VaporTeleport', () => {
+  describe('eager mode', () => {
+    runSharedTests(false)
+  })
+
+  describe('defer mode', () => {
+    runSharedTests(true)
+  })
+})
+
+function runSharedTests(deferMode: boolean): void {
+  const createComponent = deferMode
+    ? (
+        component: VaporComponent,
+        rawProps?: LooseRawProps | null,
+        ...args: any[]
+      ) => {
+        if (component === VaporTeleport) {
+          rawProps!.defer = () => true
+        }
+        return originalCreateComponent(component, rawProps, ...args)
+      }
+    : originalCreateComponent
+
+  test('should work', () => {
+    const target = document.createElement('div')
+    const root = document.createElement('div')
+
+    const { mount } = define({
+      setup() {
+        const n0 = createComponent(
+          VaporTeleport,
+          {
+            to: () => target,
+          },
+          {
+            default: () => template('<div>teleported</div>')(),
+          },
+        )
+        const n1 = template('<div>root</div>')()
+        return [n0, n1]
+      },
+    }).create()
+    mount(root)
+
+    expect(root.innerHTML).toBe('<!--teleport--><div>root</div>')
+    expect(target.innerHTML).toBe('<div>teleported</div>')
+  })
+
+  test.todo('should work with SVG', async () => {})
+
+  test('should update target', async () => {
+    const targetA = document.createElement('div')
+    const targetB = document.createElement('div')
+    const target = ref(targetA)
+    const root = document.createElement('div')
+
+    const { mount } = define({
+      setup() {
+        const n0 = createComponent(
+          VaporTeleport,
+          {
+            to: () => target.value,
+          },
+          {
+            default: () => template('<div>teleported</div>')(),
+          },
+        )
+        const n1 = template('<div>root</div>')()
+        return [n0, n1]
+      },
+    }).create()
+    mount(root)
+
+    expect(root.innerHTML).toBe('<!--teleport--><div>root</div>')
+    expect(targetA.innerHTML).toBe('<div>teleported</div>')
+    expect(targetB.innerHTML).toBe('')
+
+    target.value = targetB
+    await nextTick()
+
+    expect(root.innerHTML).toBe('<!--teleport--><div>root</div>')
+    expect(targetA.innerHTML).toBe('')
+    expect(targetB.innerHTML).toBe('<div>teleported</div>')
+  })
+
+  test('should update children', async () => {
+    const target = document.createElement('div')
+    const root = document.createElement('div')
+    const children = shallowRef([template('<div>teleported</div>')()])
+
+    const { mount } = define({
+      setup() {
+        const n0 = createComponent(
+          VaporTeleport,
+          {
+            to: () => target,
+          },
+          {
+            default: () => children.value,
+          },
+        )
+        const n1 = template('<div>root</div>')()
+        return [n0, n1]
+      },
+    }).create()
+    mount(root)
+
+    expect(target.innerHTML).toBe('<div>teleported</div>')
+
+    children.value = [template('')()]
+    await nextTick()
+    expect(target.innerHTML).toBe('')
+
+    children.value = [template('teleported')()]
+    await nextTick()
+    expect(target.innerHTML).toBe('teleported')
+  })
+
+  test('should remove children when unmounted', async () => {
+    const target = document.createElement('div')
+    const root = document.createElement('div')
+
+    function testUnmount(props: any) {
+      const { app } = define({
+        setup() {
+          const n0 = createComponent(VaporTeleport, props, {
+            default: () => template('<div>teleported</div>')(),
+          })
+          const n1 = template('<div>root</div>')()
+          return [n0, n1]
+        },
+      }).create()
+      app.mount(root)
+
+      expect(target.innerHTML).toBe(
+        props.disabled() ? '' : '<div>teleported</div>',
+      )
+
+      app.unmount()
+      expect(target.innerHTML).toBe('')
+      expect(target.children.length).toBe(0)
+    }
+
+    testUnmount({ to: () => target, disabled: () => false })
+    testUnmount({ to: () => target, disabled: () => true })
+    testUnmount({ to: () => null, disabled: () => true })
+  })
+
+  test('component with multi roots should be removed when unmounted', async () => {
+    const target = document.createElement('div')
+    const root = document.createElement('div')
+
+    const { component: Comp } = define({
+      setup() {
+        return [template('<p>')(), template('<p>')()]
+      },
+    })
+
+    const { app } = define({
+      setup() {
+        const n0 = createComponent(
+          VaporTeleport,
+          {
+            to: () => target,
+          },
+          {
+            default: () => createComponent(Comp),
+          },
+        )
+        const n1 = template('<div>root</div>')()
+        return [n0, n1]
+      },
+    }).create()
+
+    app.mount(root)
+    expect(target.innerHTML).toBe('<p></p><p></p>')
+
+    app.unmount()
+    expect(target.innerHTML).toBe('')
+  })
+
+  test.todo(
+    'descendent component should be unmounted when teleport is disabled and unmounted',
+    async () => {
+      const root = document.createElement('div')
+      const beforeUnmount = vi.fn()
+      const unmounted = vi.fn()
+      const { component: Comp } = define({
+        setup() {
+          onBeforeUnmount(beforeUnmount)
+          onUnmounted(unmounted)
+          return [template('<p>')(), template('<p>')()]
+        },
+      })
+
+      const { app } = define({
+        setup() {
+          const n0 = createComponent(
+            VaporTeleport,
+            {
+              to: () => null,
+              disabled: () => true,
+            },
+            {
+              default: () => createComponent(Comp),
+            },
+          )
+          return [n0]
+        },
+      }).create()
+      app.mount(root)
+
+      expect(beforeUnmount).toHaveBeenCalledTimes(0)
+      expect(unmounted).toHaveBeenCalledTimes(0)
+
+      app.unmount()
+      expect(beforeUnmount).toHaveBeenCalledTimes(1)
+      expect(unmounted).toHaveBeenCalledTimes(1)
+    },
+  )
+
+  test.todo('multiple teleport with same target', async () => {})
+  test.todo('should work when using template ref as target', async () => {})
+  test.todo('disabled', async () => {})
+  test.todo('moving teleport while enabled', async () => {})
+  test.todo('moving teleport while disabled', async () => {})
+  test.todo('should work with block tree', async () => {})
+  test.todo(
+    `the dir hooks of the Teleport's children should be called correctly`,
+    async () => {},
+  )
+  test.todo(
+    `ensure that target changes when disabled are updated correctly when enabled`,
+    async () => {},
+  )
+  test.todo('toggle sibling node inside target node', async () => {})
+  test.todo('unmount previous sibling node inside target node', async () => {})
+  test.todo('accessing template refs inside teleport', async () => {})
+}
index 762d69ce81b6aab73a2af764f5471ed9ff261e86..e12ad4d87ec52fa21094e8c69bab1333813af222 100644 (file)
@@ -18,6 +18,7 @@ import { createComment, createTextNode, querySelector } from '../dom/node'
 import type { LooseRawProps, LooseRawSlots } from '../component'
 import { rawPropsProxyHandlers } from '../componentProps'
 import { renderEffect } from '../renderEffect'
+import { extend } from '@vue/shared'
 
 export const VaporTeleportImpl = {
   name: 'VaporTeleport',
@@ -25,7 +26,6 @@ export const VaporTeleportImpl = {
   __vapor: true,
 
   process(props: LooseRawProps, slots: LooseRawSlots): TeleportFragment {
-    const children = slots.default && (slots.default as BlockFn)()
     const frag = __DEV__
       ? new TeleportFragment('teleport')
       : new TeleportFragment()
@@ -35,31 +35,11 @@ export const VaporTeleportImpl = {
       rawPropsProxyHandlers,
     ) as any as TeleportProps
 
-    renderEffect(() => frag.update(resolvedProps, children))
-
-    frag.remove = parent => {
-      const {
-        nodes,
-        target,
-        cachedTargetAnchor,
-        targetStart,
-        placeholder,
-        mainAnchor,
-      } = frag
-
-      remove(nodes, target || parent)
-
-      // remove anchors
-      if (targetStart) {
-        let parentNode = targetStart.parentNode!
-        remove(targetStart!, parentNode)
-        remove(cachedTargetAnchor!, parentNode)
-      }
-      if (placeholder && placeholder.isConnected) {
-        remove(placeholder!, parent)
-        remove(mainAnchor!, parent)
-      }
-    }
+    renderEffect(() => {
+      const children = slots.default && (slots.default as BlockFn)()
+      // access the props to trigger tracking
+      frag.update(extend({}, resolvedProps), children)
+    })
 
     return frag
   },
@@ -67,12 +47,10 @@ export const VaporTeleportImpl = {
 
 export class TeleportFragment extends VaporFragment {
   anchor: Node
-  target?: ParentNode | null
   targetStart?: Node | null
-  targetAnchor?: Node | null
-  cachedTargetAnchor?: Node
   mainAnchor?: Node
   placeholder?: Node
+  currentParent?: ParentNode | null
 
   constructor(anchorLabel?: string) {
     super([])
@@ -81,33 +59,21 @@ export class TeleportFragment extends VaporFragment {
   }
 
   update(props: TeleportProps, children: Block): void {
+    // teardown previous
+    if (this.currentParent && this.nodes) {
+      remove(this.nodes, this.currentParent)
+    }
+
     this.nodes = children
+    const disabled = isTeleportDisabled(props)
     const parent = this.anchor.parentNode
 
-    if (!this.mainAnchor) {
-      this.mainAnchor = __DEV__
-        ? createComment('teleport end')
-        : createTextNode()
-    }
-    if (!this.placeholder) {
-      this.placeholder = __DEV__
-        ? createComment('teleport start')
-        : createTextNode()
-    }
-    if (parent) {
-      insert(this.placeholder, parent, this.anchor)
-      insert(this.mainAnchor, parent, this.anchor)
+    const mount = (parent: ParentNode, anchor: Node | null) => {
+      insert(this.nodes, (this.currentParent = parent), anchor)
     }
 
-    const disabled = isTeleportDisabled(props)
-    if (disabled) {
-      this.target = this.anchor.parentNode
-      this.targetAnchor = parent ? this.mainAnchor : null
-    } else {
-      const target = (this.target = resolveTarget(
-        props,
-        querySelector,
-      ) as ParentNode)
+    const mountToTarget = () => {
+      const target = (this.target = resolveTarget(props, querySelector))
       if (target) {
         if (
           // initial mount
@@ -116,21 +82,38 @@ export class TeleportFragment extends VaporFragment {
           this.targetStart.parentNode !== target
         ) {
           ;[this.targetAnchor, this.targetStart] = prepareAnchor(target)
-          this.cachedTargetAnchor = this.targetAnchor
-        } else {
-          // re-mount or target not changed, use cached target anchor
-          this.targetAnchor = this.cachedTargetAnchor
         }
+
+        mount(target, this.targetAnchor!)
       } else if (__DEV__) {
-        warn('Invalid Teleport target on mount:', target, `(${typeof target})`)
+        warn(
+          `Invalid Teleport target on ${this.targetStart ? 'update' : 'mount'}:`,
+          target,
+          `(${typeof target})`,
+        )
       }
     }
 
-    const mountToTarget = () => {
-      insert(this.nodes, this.target!, this.targetAnchor)
+    if (parent && disabled) {
+      if (!this.mainAnchor) {
+        this.mainAnchor = __DEV__
+          ? createComment('teleport end')
+          : createTextNode()
+      }
+      if (!this.placeholder) {
+        this.placeholder = __DEV__
+          ? createComment('teleport start')
+          : createTextNode()
+      }
+      if (!this.mainAnchor.isConnected) {
+        insert(this.placeholder, parent, this.anchor)
+        insert(this.mainAnchor, parent, this.anchor)
+      }
+
+      mount(parent, this.mainAnchor)
     }
 
-    if (parent) {
+    if (!disabled) {
       if (isTeleportDeferred(props)) {
         queuePostFlushCb(mountToTarget)
       } else {
@@ -139,14 +122,30 @@ export class TeleportFragment extends VaporFragment {
     }
   }
 
+  remove = (parent: ParentNode | undefined): void => {
+    // remove nodes
+    remove(this.nodes, this.currentParent || parent)
+
+    // remove anchors
+    if (this.targetStart) {
+      let parentNode = this.targetStart.parentNode!
+      remove(this.targetStart!, parentNode)
+      remove(this.targetAnchor!, parentNode)
+    }
+    if (this.placeholder) {
+      remove(this.placeholder!, parent)
+      remove(this.mainAnchor!, parent)
+    }
+  }
+
   hydrate(): void {
     // TODO
   }
 }
 
 function prepareAnchor(target: ParentNode | null) {
-  const targetStart = createTextNode('targetStart')
-  const targetAnchor = createTextNode('targetAnchor')
+  const targetStart = createTextNode('')
+  const targetAnchor = createTextNode('')
 
   // attach a special property, so we can skip teleported content in
   // renderer's nextSibling search