52cc7e8231
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
|
|
])
|
|
}
|