feat(directives): add support for function directives (#252)
This commit is contained in:
parent
a72652f6e6
commit
0bac763f5a
@ -144,4 +144,58 @@ describe('directives', () => {
|
|||||||
expect(beforeUnmount).toHaveBeenCalled()
|
expect(beforeUnmount).toHaveBeenCalled()
|
||||||
expect(unmounted).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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -34,7 +34,7 @@ export type DirectiveHook<T = any> = (
|
|||||||
prevVNode: VNode<any, T> | null
|
prevVNode: VNode<any, T> | null
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
export interface Directive<T = any> {
|
export interface ObjectDirective<T = any> {
|
||||||
beforeMount?: DirectiveHook<T>
|
beforeMount?: DirectiveHook<T>
|
||||||
mounted?: DirectiveHook<T>
|
mounted?: DirectiveHook<T>
|
||||||
beforeUpdate?: DirectiveHook<T>
|
beforeUpdate?: DirectiveHook<T>
|
||||||
@ -43,6 +43,10 @@ export interface Directive<T = any> {
|
|||||||
unmounted?: DirectiveHook<T>
|
unmounted?: DirectiveHook<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FunctionDirective<T = any> = DirectiveHook<T>
|
||||||
|
|
||||||
|
export type Directive<T = any> = ObjectDirective<T> | FunctionDirective<T>
|
||||||
|
|
||||||
type DirectiveModifiers = Record<string, boolean>
|
type DirectiveModifiers = Record<string, boolean>
|
||||||
|
|
||||||
const valueCache = new WeakMap<Directive, WeakMap<any, any>>()
|
const valueCache = new WeakMap<Directive, WeakMap<any, any>>()
|
||||||
@ -60,8 +64,16 @@ function applyDirective(
|
|||||||
valueCacheForDir = new WeakMap<VNode, any>()
|
valueCacheForDir = new WeakMap<VNode, any>()
|
||||||
valueCache.set(directive, valueCacheForDir)
|
valueCache.set(directive, valueCacheForDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFunction(directive)) {
|
||||||
|
directive = {
|
||||||
|
mounted: directive,
|
||||||
|
updated: directive
|
||||||
|
} as ObjectDirective
|
||||||
|
}
|
||||||
|
|
||||||
for (const key in directive) {
|
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 hookKey = `vnode` + key[0].toUpperCase() + key.slice(1)
|
||||||
const vnodeHook = (vnode: VNode, prevVNode: VNode | null) => {
|
const vnodeHook = (vnode: VNode, prevVNode: VNode | null) => {
|
||||||
let oldValue
|
let oldValue
|
||||||
|
@ -81,6 +81,8 @@ export {
|
|||||||
Directive,
|
Directive,
|
||||||
DirectiveBinding,
|
DirectiveBinding,
|
||||||
DirectiveHook,
|
DirectiveHook,
|
||||||
|
ObjectDirective,
|
||||||
|
FunctionDirective,
|
||||||
DirectiveArguments
|
DirectiveArguments
|
||||||
} from './directives'
|
} from './directives'
|
||||||
export { SuspenseBoundary } from './suspense'
|
export { SuspenseBoundary } from './suspense'
|
||||||
|
@ -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 { addEventListener } from '../modules/events'
|
||||||
import { isArray, isObject } from '@vue/shared'
|
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
|
// 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.
|
// 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) {
|
beforeMount(el, { value, modifiers: { lazy, trim, number } }, vnode) {
|
||||||
el.value = value
|
el.value = value
|
||||||
const assign = getModelAssigner(vnode)
|
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) {
|
beforeMount(el, binding, vnode) {
|
||||||
setChecked(el, binding, vnode)
|
setChecked(el, binding, vnode)
|
||||||
const assign = getModelAssigner(vnode)
|
const assign = getModelAssigner(vnode)
|
||||||
@ -111,7 +116,7 @@ function setChecked(
|
|||||||
: !!value
|
: !!value
|
||||||
}
|
}
|
||||||
|
|
||||||
export const vModelRadio: Directive<HTMLInputElement> = {
|
export const vModelRadio: ObjectDirective<HTMLInputElement> = {
|
||||||
beforeMount(el, { value }, vnode) {
|
beforeMount(el, { value }, vnode) {
|
||||||
el.checked = looseEqual(value, vnode.props!.value)
|
el.checked = looseEqual(value, vnode.props!.value)
|
||||||
const assign = getModelAssigner(vnode)
|
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.
|
// use mounted & updated because <select> relies on its children <option>s.
|
||||||
mounted(el, { value }, vnode) {
|
mounted(el, { value }, vnode) {
|
||||||
setSelected(el, value)
|
setSelected(el, value)
|
||||||
@ -214,7 +219,7 @@ function getValue(el: HTMLOptionElement | HTMLInputElement) {
|
|||||||
return '_value' in el ? (el as any)._value : el.value
|
return '_value' in el ? (el as any)._value : el.value
|
||||||
}
|
}
|
||||||
|
|
||||||
export const vModelDynamic: Directive<
|
export const vModelDynamic: ObjectDirective<
|
||||||
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
||||||
> = {
|
> = {
|
||||||
beforeMount(el, binding, vnode) {
|
beforeMount(el, binding, vnode) {
|
||||||
@ -236,9 +241,9 @@ function callModelHook(
|
|||||||
binding: DirectiveBinding,
|
binding: DirectiveBinding,
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
prevVNode: VNode | null,
|
prevVNode: VNode | null,
|
||||||
hook: keyof Directive
|
hook: keyof ObjectDirective
|
||||||
) {
|
) {
|
||||||
let modelToUse: Directive
|
let modelToUse: ObjectDirective
|
||||||
switch (el.tagName) {
|
switch (el.tagName) {
|
||||||
case 'SELECT':
|
case 'SELECT':
|
||||||
modelToUse = vModelSelect
|
modelToUse = vModelSelect
|
||||||
|
Loading…
x
Reference in New Issue
Block a user