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

feat(directives): add support for function directives (#252)

Dmitry Sharshakov 6 лет назад
Родитель
Сommit
0bac763f5a

+ 54 - 0
packages/runtime-core/__tests__/directives.spec.ts

@@ -144,4 +144,58 @@ describe('directives', () => {
     expect(beforeUnmount).toHaveBeenCalled()
     expect(unmounted).toHaveBeenCalled()
   })
+
+  it('should work with a function directive', async () => {
+    const count = ref(0)
+
+    function assertBindings(binding: DirectiveBinding) {
+      expect(binding.value).toBe(count.value)
+      expect(binding.arg).toBe('foo')
+      expect(binding.instance).toBe(_instance && _instance.renderProxy)
+      expect(binding.modifiers && binding.modifiers.ok).toBe(true)
+    }
+
+    const fn = jest.fn(((el, binding, vnode, prevVNode) => {
+      expect(el.tag).toBe('div')
+      expect(el.parentNode).toBe(root)
+
+      assertBindings(binding)
+
+      expect(vnode).toBe(_vnode)
+      expect(prevVNode).toBe(_prevVnode)
+    }) as DirectiveHook)
+
+    let _instance: ComponentInternalInstance | null = null
+    let _vnode: VNode | null = null
+    let _prevVnode: VNode | null = null
+    const Comp = {
+      setup() {
+        _instance = currentInstance
+      },
+      render() {
+        _prevVnode = _vnode
+        _vnode = applyDirectives(h('div', count.value), [
+          [
+            fn,
+            // value
+            count.value,
+            // argument
+            'foo',
+            // modifiers
+            { ok: true }
+          ]
+        ])
+        return _vnode
+      }
+    }
+
+    const root = nodeOps.createElement('div')
+    render(h(Comp), root)
+
+    expect(fn).toHaveBeenCalledTimes(1)
+
+    count.value++
+    await nextTick()
+    expect(fn).toHaveBeenCalledTimes(2)
+  })
 })

+ 14 - 2
packages/runtime-core/src/directives.ts

@@ -34,7 +34,7 @@ export type DirectiveHook<T = any> = (
   prevVNode: VNode<any, T> | null
 ) => void
 
-export interface Directive<T = any> {
+export interface ObjectDirective<T = any> {
   beforeMount?: DirectiveHook<T>
   mounted?: DirectiveHook<T>
   beforeUpdate?: DirectiveHook<T>
@@ -43,6 +43,10 @@ export interface Directive<T = any> {
   unmounted?: DirectiveHook<T>
 }
 
+export type FunctionDirective<T = any> = DirectiveHook<T>
+
+export type Directive<T = any> = ObjectDirective<T> | FunctionDirective<T>
+
 type DirectiveModifiers = Record<string, boolean>
 
 const valueCache = new WeakMap<Directive, WeakMap<any, any>>()
@@ -60,8 +64,16 @@ function applyDirective(
     valueCacheForDir = new WeakMap<VNode, any>()
     valueCache.set(directive, valueCacheForDir)
   }
+
+  if (isFunction(directive)) {
+    directive = {
+      mounted: directive,
+      updated: directive
+    } as ObjectDirective
+  }
+
   for (const key in directive) {
-    const hook = directive[key as keyof Directive]!
+    const hook = directive[key as keyof ObjectDirective]!
     const hookKey = `vnode` + key[0].toUpperCase() + key.slice(1)
     const vnodeHook = (vnode: VNode, prevVNode: VNode | null) => {
       let oldValue

+ 2 - 0
packages/runtime-core/src/index.ts

@@ -81,6 +81,8 @@ export {
   Directive,
   DirectiveBinding,
   DirectiveHook,
+  ObjectDirective,
+  FunctionDirective,
   DirectiveArguments
 } from './directives'
 export { SuspenseBoundary } from './suspense'

+ 13 - 8
packages/runtime-dom/src/directives/vModel.ts

@@ -1,4 +1,9 @@
-import { Directive, VNode, DirectiveBinding, warn } from '@vue/runtime-core'
+import {
+  ObjectDirective,
+  VNode,
+  DirectiveBinding,
+  warn
+} from '@vue/runtime-core'
 import { addEventListener } from '../modules/events'
 import { isArray, isObject } from '@vue/shared'
 
@@ -30,7 +35,7 @@ function toNumber(val: string): number | string {
 
 // We are exporting the v-model runtime directly as vnode hooks so that it can
 // be tree-shaken in case v-model is never used.
-export const vModelText: Directive<HTMLInputElement | HTMLTextAreaElement> = {
+export const vModelText: ObjectDirective<HTMLInputElement | HTMLTextAreaElement> = {
   beforeMount(el, { value, modifiers: { lazy, trim, number } }, vnode) {
     el.value = value
     const assign = getModelAssigner(vnode)
@@ -72,7 +77,7 @@ export const vModelText: Directive<HTMLInputElement | HTMLTextAreaElement> = {
   }
 }
 
-export const vModelCheckbox: Directive<HTMLInputElement> = {
+export const vModelCheckbox: ObjectDirective<HTMLInputElement> = {
   beforeMount(el, binding, vnode) {
     setChecked(el, binding, vnode)
     const assign = getModelAssigner(vnode)
@@ -111,7 +116,7 @@ function setChecked(
     : !!value
 }
 
-export const vModelRadio: Directive<HTMLInputElement> = {
+export const vModelRadio: ObjectDirective<HTMLInputElement> = {
   beforeMount(el, { value }, vnode) {
     el.checked = looseEqual(value, vnode.props!.value)
     const assign = getModelAssigner(vnode)
@@ -124,7 +129,7 @@ export const vModelRadio: Directive<HTMLInputElement> = {
   }
 }
 
-export const vModelSelect: Directive<HTMLSelectElement> = {
+export const vModelSelect: ObjectDirective<HTMLSelectElement> = {
   // use mounted & updated because <select> relies on its children <option>s.
   mounted(el, { value }, vnode) {
     setSelected(el, value)
@@ -214,7 +219,7 @@ function getValue(el: HTMLOptionElement | HTMLInputElement) {
   return '_value' in el ? (el as any)._value : el.value
 }
 
-export const vModelDynamic: Directive<
+export const vModelDynamic: ObjectDirective<
   HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
 > = {
   beforeMount(el, binding, vnode) {
@@ -236,9 +241,9 @@ function callModelHook(
   binding: DirectiveBinding,
   vnode: VNode,
   prevVNode: VNode | null,
-  hook: keyof Directive
+  hook: keyof ObjectDirective
 ) {
-  let modelToUse: Directive
+  let modelToUse: ObjectDirective
   switch (el.tagName) {
     case 'SELECT':
       modelToUse = vModelSelect