Просмотр исходного кода

fix(vapor): support directives on vapor components in vdom parent (#14355)

edison 2 месяцев назад
Родитель
Сommit
9add6d7b4f

+ 7 - 1
packages/runtime-core/src/apiCreateApp.ts

@@ -189,8 +189,14 @@ export interface VaporInteropInterface {
     anchor: any,
     anchor: any,
     parentComponent: ComponentInternalInstance | null,
     parentComponent: ComponentInternalInstance | null,
     parentSuspense: SuspenseBoundary | null,
     parentSuspense: SuspenseBoundary | null,
+    onBeforeMount?: () => void,
   ): GenericComponentInstance // VaporComponentInstance
   ): GenericComponentInstance // VaporComponentInstance
-  update(n1: VNode, n2: VNode, shouldUpdate: boolean): void
+  update(
+    n1: VNode,
+    n2: VNode,
+    shouldUpdate: boolean,
+    onBeforeUpdate?: () => void,
+  ): void
   unmount(vnode: VNode, doRemove?: boolean): void
   unmount(vnode: VNode, doRemove?: boolean): void
   move(vnode: VNode, container: any, anchor: any, moveType: MoveType): void
   move(vnode: VNode, container: any, anchor: any, moveType: MoveType): void
   slot(
   slot(

+ 37 - 0
packages/runtime-core/src/renderer.ts

@@ -1221,14 +1221,39 @@ function baseCreateRenderer(
             anchor,
             anchor,
             parentComponent,
             parentComponent,
             parentSuspense,
             parentSuspense,
+            () => {
+              if (n2.dirs) {
+                invokeDirectiveHook(n2, null, parentComponent, 'created')
+                invokeDirectiveHook(n2, null, parentComponent, 'beforeMount')
+              }
+            },
           )
           )
+          if (n2.dirs) {
+            queuePostRenderEffect(
+              () => invokeDirectiveHook(n2, null, parentComponent, 'mounted'),
+              undefined,
+              parentSuspense,
+            )
+          }
         }
         }
       } else {
       } else {
         getVaporInterface(parentComponent, n2).update(
         getVaporInterface(parentComponent, n2).update(
           n1,
           n1,
           n2,
           n2,
           shouldUpdateComponent(n1, n2, optimized),
           shouldUpdateComponent(n1, n2, optimized),
+          () => {
+            if (n2.dirs) {
+              invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
+            }
+          },
         )
         )
+        if (n2.dirs) {
+          queuePostRenderEffect(
+            () => invokeDirectiveHook(n2, n1, parentComponent, 'updated'),
+            undefined,
+            parentSuspense,
+          )
+        }
       }
       }
     } else if (n1 == null) {
     } else if (n1 == null) {
       if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
       if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
@@ -2366,7 +2391,19 @@ function baseCreateRenderer(
 
 
     if (shapeFlag & ShapeFlags.COMPONENT) {
     if (shapeFlag & ShapeFlags.COMPONENT) {
       if (isVaporComponent(type as ConcreteComponent)) {
       if (isVaporComponent(type as ConcreteComponent)) {
+        // invoke directive hooks for vapor components
+        if (dirs) {
+          invokeDirectiveHook(vnode, null, parentComponent, 'beforeUnmount')
+        }
         getVaporInterface(parentComponent, vnode).unmount(vnode, doRemove)
         getVaporInterface(parentComponent, vnode).unmount(vnode, doRemove)
+        if (dirs) {
+          queuePostRenderEffect(
+            () =>
+              invokeDirectiveHook(vnode, null, parentComponent, 'unmounted'),
+            undefined,
+            parentSuspense,
+          )
+        }
         return
         return
       } else {
       } else {
         unmountComponent(vnode.component!, parentSuspense, doRemove)
         unmountComponent(vnode.component!, parentSuspense, doRemove)

+ 170 - 1
packages/runtime-vapor/__tests__/vdomInterop.spec.ts

@@ -1,6 +1,7 @@
 import {
 import {
   KeepAlive,
   KeepAlive,
   type ShallowRef,
   type ShallowRef,
+  createApp,
   createVNode,
   createVNode,
   defineComponent,
   defineComponent,
   h,
   h,
@@ -19,6 +20,8 @@ import {
   toDisplayString,
   toDisplayString,
   useModel,
   useModel,
   useTemplateRef,
   useTemplateRef,
+  vShow,
+  withDirectives,
 } from '@vue/runtime-dom'
 } from '@vue/runtime-dom'
 import { makeInteropRender } from './_utils'
 import { makeInteropRender } from './_utils'
 import {
 import {
@@ -35,6 +38,7 @@ import {
   renderEffect,
   renderEffect,
   setText,
   setText,
   template,
   template,
+  vaporInteropPlugin,
 } from '../src'
 } from '../src'
 
 
 const define = makeInteropRender()
 const define = makeInteropRender()
@@ -231,7 +235,7 @@ describe('vdomInterop', () => {
     })
     })
   })
   })
 
 
-  describe('v-show', () => {
+  describe('directives', () => {
     test('apply v-show to vdom child', async () => {
     test('apply v-show to vdom child', async () => {
       const VDomChild = {
       const VDomChild = {
         setup() {
         setup() {
@@ -260,6 +264,171 @@ describe('vdomInterop', () => {
       await nextTick()
       await nextTick()
       expect(html()).toBe('<div style=""></div>')
       expect(html()).toBe('<div style=""></div>')
     })
     })
+
+    test('apply v-show to vapor child', async () => {
+      const VaporChild = defineVaporComponent({
+        setup() {
+          return template('<div></div>', true)()
+        },
+      })
+
+      const show = ref(false)
+      const App = defineComponent({
+        setup() {
+          return () =>
+            h('div', null, [
+              withDirectives(h(VaporChild as any), [[vShow, show.value]]),
+            ])
+        },
+      })
+
+      const root = document.createElement('div')
+      const app = createApp(App)
+      app.use(vaporInteropPlugin)
+      app.mount(root)
+
+      expect(root.innerHTML).toBe(
+        '<div><div style="display: none;"></div></div>',
+      )
+
+      show.value = true
+      await nextTick()
+      expect(root.innerHTML).toBe('<div><div style=""></div></div>')
+    })
+
+    test('apply custom directive to vapor child', async () => {
+      const vCustom = {
+        created: vi.fn(),
+        beforeMount: vi.fn(),
+        mounted: vi.fn(),
+        beforeUpdate: vi.fn(),
+        updated: vi.fn(),
+        beforeUnmount: vi.fn(),
+        unmounted: vi.fn(),
+      }
+
+      const VaporChild = defineVaporComponent({
+        setup() {
+          return template('<div></div>', true)()
+        },
+      })
+
+      const count = ref(0)
+      const App = defineComponent({
+        setup() {
+          return () =>
+            h('div', null, [
+              withDirectives(h(VaporChild as any), [[vCustom, count.value]]),
+            ])
+        },
+      })
+
+      const root = document.createElement('div')
+      const app = createApp(App)
+      app.use(vaporInteropPlugin)
+      app.mount(root)
+
+      // root > div (App root) > div (VaporChild root)
+      const el = root.querySelector('div')!.querySelector('div')!
+      expect(vCustom.created).toHaveBeenCalledTimes(1)
+      expect(vCustom.beforeMount).toHaveBeenCalledTimes(1)
+      expect(vCustom.mounted).toHaveBeenCalledTimes(1)
+      expect(vCustom.beforeUpdate).toHaveBeenCalledTimes(0)
+      expect(vCustom.updated).toHaveBeenCalledTimes(0)
+
+      expect(vCustom.created).toHaveBeenCalledWith(
+        el,
+        expect.objectContaining({ value: 0, oldValue: undefined }),
+        expect.any(Object),
+        null,
+      )
+      expect(vCustom.beforeMount).toHaveBeenCalledWith(
+        el,
+        expect.objectContaining({ value: 0, oldValue: undefined }),
+        expect.any(Object),
+        null,
+      )
+      expect(vCustom.mounted).toHaveBeenCalledWith(
+        el,
+        expect.objectContaining({ value: 0, oldValue: undefined }),
+        expect.any(Object),
+        null,
+      )
+
+      count.value++
+      await nextTick()
+      expect(vCustom.beforeUpdate).toHaveBeenCalledTimes(1)
+      expect(vCustom.updated).toHaveBeenCalledTimes(1)
+
+      expect(vCustom.beforeUpdate).toHaveBeenCalledWith(
+        el,
+        expect.objectContaining({ value: 1, oldValue: 0 }),
+        expect.any(Object),
+        expect.any(Object),
+      )
+      expect(vCustom.updated).toHaveBeenCalledWith(
+        el,
+        expect.objectContaining({ value: 1, oldValue: 0 }),
+        expect.any(Object),
+        expect.any(Object),
+      )
+
+      app.unmount()
+      expect(vCustom.beforeUnmount).toHaveBeenCalledTimes(1)
+      expect(vCustom.unmounted).toHaveBeenCalledTimes(1)
+
+      expect(vCustom.beforeUnmount).toHaveBeenCalledWith(
+        el,
+        expect.objectContaining({ value: 1, oldValue: 0 }),
+        expect.any(Object),
+        null,
+      )
+      expect(vCustom.unmounted).toHaveBeenCalledWith(
+        el,
+        expect.objectContaining({ value: 1, oldValue: 0 }),
+        expect.any(Object),
+        null,
+      )
+    })
+
+    test('warn on directive with non-element root vapor child', () => {
+      const calls: string[] = []
+      const vCustom = {
+        created: () => calls.push('created'),
+        beforeMount: () => calls.push('beforeMount'),
+        mounted: () => calls.push('mounted'),
+        beforeUpdate: () => calls.push('beforeUpdate'),
+        updated: () => calls.push('updated'),
+        beforeUnmount: () => calls.push('beforeUnmount'),
+        unmounted: () => calls.push('unmounted'),
+      }
+
+      const VaporChild = defineVaporComponent({
+        setup() {
+          return [template('<div></div>')(), template('<div></div>')()]
+        },
+      })
+
+      const App = defineComponent({
+        setup() {
+          return () =>
+            h('div', null, [withDirectives(h(VaporChild as any), [[vCustom]])])
+        },
+      })
+
+      const root = document.createElement('div')
+      const app = createApp(App)
+      app.use(vaporInteropPlugin)
+      app.mount(root)
+
+      if (__DEV__) {
+        expect(
+          `Runtime directive used on component with non-element root node.`,
+        ).toHaveBeenWarned()
+      }
+      expect(calls.length).toBe(0)
+      app.unmount()
+    })
   })
   })
 
 
   describe('slots', () => {
   describe('slots', () => {

+ 47 - 3
packages/runtime-vapor/src/vdomInterop.ts

@@ -37,6 +37,7 @@ import {
   activate as vdomActivate,
   activate as vdomActivate,
   deactivate as vdomDeactivate,
   deactivate as vdomDeactivate,
   setRef as vdomSetRef,
   setRef as vdomSetRef,
+  warn,
 } from '@vue/runtime-dom'
 } from '@vue/runtime-dom'
 import {
 import {
   type LooseRawProps,
   type LooseRawProps,
@@ -45,6 +46,7 @@ import {
   VaporComponentInstance,
   VaporComponentInstance,
   createComponent,
   createComponent,
   getCurrentScopeId,
   getCurrentScopeId,
+  getRootElement,
   mountComponent,
   mountComponent,
   unmountComponent,
   unmountComponent,
 } from './component'
 } from './component'
@@ -97,7 +99,14 @@ const vaporInteropImpl: Omit<
   VaporInteropInterface,
   VaporInteropInterface,
   'vdomMount' | 'vdomUnmount' | 'vdomSlot' | 'vdomMountVNode'
   'vdomMount' | 'vdomUnmount' | 'vdomSlot' | 'vdomMountVNode'
 > = {
 > = {
-  mount(vnode, container, anchor, parentComponent, parentSuspense) {
+  mount(
+    vnode,
+    container,
+    anchor,
+    parentComponent,
+    parentSuspense,
+    onBeforeMount,
+  ) {
     let selfAnchor = (vnode.anchor = createTextNode())
     let selfAnchor = (vnode.anchor = createTextNode())
     if (isHydrating) {
     if (isHydrating) {
       // avoid vdom hydration children mismatch by the selfAnchor, delay its insertion
       // avoid vdom hydration children mismatch by the selfAnchor, delay its insertion
@@ -160,16 +169,51 @@ const vaporInteropImpl: Omit<
       setParentSuspense(prevSuspense)
       setParentSuspense(prevSuspense)
     }
     }
 
 
+    const rootEl = getRootElement(instance)
+    if (rootEl) {
+      vnode.el = rootEl
+    }
+    // invoke directive hooks only when we have a valid root element
+    if (vnode.dirs) {
+      if (rootEl) {
+        onBeforeMount && onBeforeMount()
+      } else {
+        if (__DEV__) {
+          warn(
+            `Runtime directive used on component with non-element root node. ` +
+              `The directives will not function as intended.`,
+          )
+        }
+        vnode.dirs = null
+      }
+    }
+
     mountComponent(instance, container, selfAnchor)
     mountComponent(instance, container, selfAnchor)
+
     simpleSetCurrentInstance(prev)
     simpleSetCurrentInstance(prev)
     return instance
     return instance
   },
   },
 
 
-  update(n1, n2, shouldUpdate) {
+  update(n1, n2, shouldUpdate, onBeforeUpdate) {
     n2.component = n1.component
     n2.component = n1.component
     n2.el = n2.anchor = n1.anchor
     n2.el = n2.anchor = n1.anchor
+
+    const instance = n2.component as any as VaporComponentInstance
+
+    const rootEl = getRootElement(instance)
+    if (rootEl) {
+      n2.el = rootEl
+    }
+    // invoke directive hooks only when we have a valid root element
+    if (n2.dirs) {
+      if (rootEl) {
+        onBeforeUpdate && onBeforeUpdate()
+      } else {
+        n2.dirs = null
+      }
+    }
+
     if (shouldUpdate) {
     if (shouldUpdate) {
-      const instance = n2.component as any as VaporComponentInstance
       instance.rawPropsRef!.value = n2.props
       instance.rawPropsRef!.value = n2.props
       instance.rawSlotsRef!.value = n2.children
       instance.rawSlotsRef!.value = n2.children
     }
     }