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

This commit is contained in:
Dmitry Sharshakov 2019-10-16 09:12:26 +03:00 committed by Evan You
parent a72652f6e6
commit 0bac763f5a
4 changed files with 83 additions and 10 deletions

View File

@ -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)
})
})

View File

@ -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

View File

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

View File

@ -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