BREAKING CHANGE: custom directive bindings no longer expose instance
This is a rarely used property that creates extra complexity in
ensuring it points to the correct instance. From a design
perspective, a custom directive should be scoped to the element and
data it is bound to and should not have access to the entire
instance in the first place.
151 lines
3.9 KiB
TypeScript
151 lines
3.9 KiB
TypeScript
/**
|
|
Runtime helper for applying directives to a vnode. Example usage:
|
|
|
|
const comp = resolveComponent('comp')
|
|
const foo = resolveDirective('foo')
|
|
const bar = resolveDirective('bar')
|
|
|
|
return withDirectives(h(comp), [
|
|
[foo, this.x],
|
|
[bar, this.y]
|
|
])
|
|
*/
|
|
|
|
import { VNode } from './vnode'
|
|
import { isFunction, EMPTY_OBJ, makeMap, EMPTY_ARR } from '@vue/shared'
|
|
import { warn } from './warning'
|
|
import { ComponentInternalInstance } from './component'
|
|
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
|
|
|
|
export interface DirectiveBinding {
|
|
value: any
|
|
oldValue: any
|
|
arg?: string
|
|
modifiers: DirectiveModifiers
|
|
dir: ObjectDirective
|
|
}
|
|
|
|
export type DirectiveHook<T = any> = (
|
|
el: T,
|
|
binding: DirectiveBinding,
|
|
vnode: VNode<any, T>,
|
|
prevVNode: VNode<any, T> | null
|
|
) => void
|
|
|
|
export interface ObjectDirective<T = any> {
|
|
beforeMount?: DirectiveHook<T>
|
|
mounted?: DirectiveHook<T>
|
|
beforeUpdate?: DirectiveHook<T>
|
|
updated?: DirectiveHook<T>
|
|
beforeUnmount?: DirectiveHook<T>
|
|
unmounted?: DirectiveHook<T>
|
|
}
|
|
|
|
export type FunctionDirective<T = any> = DirectiveHook<T>
|
|
|
|
export type Directive<T = any> = ObjectDirective<T> | FunctionDirective<T>
|
|
|
|
export type DirectiveModifiers = Record<string, boolean>
|
|
|
|
export type VNodeDirectiveData = [
|
|
unknown,
|
|
string | undefined,
|
|
DirectiveModifiers
|
|
]
|
|
|
|
const isBuiltInDirective = /*#__PURE__*/ makeMap(
|
|
'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text'
|
|
)
|
|
|
|
export function validateDirectiveName(name: string) {
|
|
if (isBuiltInDirective(name)) {
|
|
warn('Do not use built-in directive ids as custom directive id: ' + name)
|
|
}
|
|
}
|
|
|
|
const directiveToVnodeHooksMap = /*#__PURE__*/ [
|
|
'beforeMount',
|
|
'mounted',
|
|
'beforeUpdate',
|
|
'updated',
|
|
'beforeUnmount',
|
|
'unmounted'
|
|
].reduce(
|
|
(map, key: keyof ObjectDirective) => {
|
|
const vnodeKey = `onVnode` + key[0].toUpperCase() + key.slice(1)
|
|
const vnodeHook = (vnode: VNode, prevVnode: VNode | null) => {
|
|
const bindings = vnode.dirs!
|
|
const prevBindings = prevVnode ? prevVnode.dirs! : EMPTY_ARR
|
|
for (let i = 0; i < bindings.length; i++) {
|
|
const binding = bindings[i]
|
|
const hook = binding.dir[key]
|
|
if (hook != null) {
|
|
if (prevVnode != null) {
|
|
binding.oldValue = prevBindings[i].value
|
|
}
|
|
hook(vnode.el, binding, vnode, prevVnode)
|
|
}
|
|
}
|
|
}
|
|
map[key] = [vnodeKey, vnodeHook]
|
|
return map
|
|
},
|
|
{} as Record<string, [string, Function]>
|
|
)
|
|
|
|
// Directive, value, argument, modifiers
|
|
export type DirectiveArguments = Array<
|
|
| [Directive]
|
|
| [Directive, any]
|
|
| [Directive, any, string]
|
|
| [Directive, any, string, DirectiveModifiers]
|
|
>
|
|
|
|
export function withDirectives<T extends VNode>(
|
|
vnode: T,
|
|
directives: DirectiveArguments
|
|
): T {
|
|
const props = vnode.props || (vnode.props = {})
|
|
const bindings: DirectiveBinding[] =
|
|
vnode.dirs || (vnode.dirs = new Array(directives.length))
|
|
const injected: Record<string, true> = {}
|
|
for (let i = 0; i < directives.length; i++) {
|
|
let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
|
|
if (isFunction(dir)) {
|
|
dir = {
|
|
mounted: dir,
|
|
updated: dir
|
|
} as ObjectDirective
|
|
}
|
|
bindings[i] = {
|
|
dir,
|
|
value,
|
|
oldValue: void 0,
|
|
arg,
|
|
modifiers
|
|
}
|
|
// inject onVnodeXXX hooks
|
|
for (const key in dir) {
|
|
if (!injected[key]) {
|
|
const { 0: hookName, 1: hook } = directiveToVnodeHooksMap[key]
|
|
const existing = props[hookName]
|
|
props[hookName] = existing ? [].concat(existing, hook as any) : hook
|
|
injected[key] = true
|
|
}
|
|
}
|
|
}
|
|
return vnode
|
|
}
|
|
|
|
export function invokeDirectiveHook(
|
|
hook: ((...args: any[]) => any) | ((...args: any[]) => any)[],
|
|
instance: ComponentInternalInstance | null,
|
|
vnode: VNode,
|
|
prevVNode: VNode | null = null
|
|
) {
|
|
callWithAsyncErrorHandling(hook, instance, ErrorCodes.DIRECTIVE_HOOK, [
|
|
vnode,
|
|
prevVNode
|
|
])
|
|
}
|