vue3-core/packages/runtime-core/src/directives.ts

199 lines
5.4 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 type { VNode } from './vnode'
import { EMPTY_OBJ, isBuiltInDirective, isFunction } from '@vue/shared'
import { warn } from './warning'
import {
type ComponentInternalInstance,
type Data,
getComponentPublicInstance,
} from './component'
import { currentRenderingInstance } from './componentRenderContext'
import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
import type { ComponentPublicInstance } from './componentPublicInstance'
import { mapCompatDirectiveHook } from './compat/customDirective'
import { pauseTracking, resetTracking, traverse } from '@vue/reactivity'
export interface DirectiveBinding<
Value = any,
Modifiers extends string = string,
Arg extends string = string,
> {
instance: ComponentPublicInstance | Record<string, any> | null
value: Value
oldValue: Value | null
arg?: Arg
modifiers: DirectiveModifiers<Modifiers>
dir: ObjectDirective<any, Value>
}
export type DirectiveHook<
HostElement = any,
Prev = VNode<any, HostElement> | null,
Value = any,
Modifiers extends string = string,
Arg extends string = string,
> = (
el: HostElement,
binding: DirectiveBinding<Value, Modifiers, Arg>,
vnode: VNode<any, HostElement>,
prevVNode: Prev,
) => void
export type SSRDirectiveHook<
Value = any,
Modifiers extends string = string,
Arg extends string = string,
> = (
binding: DirectiveBinding<Value, Modifiers, Arg>,
vnode: VNode,
) => Data | undefined
export interface ObjectDirective<
HostElement = any,
Value = any,
Modifiers extends string = string,
Arg extends string = string,
> {
/**
* @internal without this, ts-expect-error in directives.test-d.ts somehow
* fails when running tsc, but passes in IDE and when testing against built
* dts. Could be a TS bug.
*/
__mod?: Modifiers
created?: DirectiveHook<HostElement, null, Value, Modifiers, Arg>
beforeMount?: DirectiveHook<HostElement, null, Value, Modifiers, Arg>
mounted?: DirectiveHook<HostElement, null, Value, Modifiers, Arg>
beforeUpdate?: DirectiveHook<
HostElement,
VNode<any, HostElement>,
Value,
Modifiers,
Arg
>
updated?: DirectiveHook<
HostElement,
VNode<any, HostElement>,
Value,
Modifiers,
Arg
>
beforeUnmount?: DirectiveHook<HostElement, null, Value, Modifiers, Arg>
unmounted?: DirectiveHook<HostElement, null, Value, Modifiers, Arg>
getSSRProps?: SSRDirectiveHook<Value, Modifiers, Arg>
deep?: boolean
}
export type FunctionDirective<
HostElement = any,
V = any,
Modifiers extends string = string,
Arg extends string = string,
> = DirectiveHook<HostElement, any, V, Modifiers, Arg>
export type Directive<
HostElement = any,
Value = any,
Modifiers extends string = string,
Arg extends string = string,
> =
| ObjectDirective<HostElement, Value, Modifiers, Arg>
| FunctionDirective<HostElement, Value, Modifiers, Arg>
export type DirectiveModifiers<K extends string = string> = Record<K, boolean>
export function validateDirectiveName(name: string): void {
if (isBuiltInDirective(name)) {
warn('Do not use built-in directive ids as custom directive id: ' + name)
}
}
// Directive, value, argument, modifiers
export type DirectiveArguments = Array<
| [Directive | undefined]
| [Directive | undefined, any]
| [Directive | undefined, any, string]
| [Directive | undefined, any, string | undefined, DirectiveModifiers]
>
/**
* Adds directives to a VNode.
*/
export function withDirectives<T extends VNode>(
vnode: T,
directives: DirectiveArguments,
): T {
if (currentRenderingInstance === null) {
__DEV__ && warn(`withDirectives can only be used inside render functions.`)
return vnode
}
const instance = getComponentPublicInstance(currentRenderingInstance)
const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = [])
for (let i = 0; i < directives.length; i++) {
let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
if (dir) {
if (isFunction(dir)) {
dir = {
mounted: dir,
updated: dir,
} as ObjectDirective
}
if (dir.deep) {
traverse(value)
}
bindings.push({
dir,
instance,
value,
oldValue: void 0,
arg,
modifiers,
})
}
}
return vnode
}
export function invokeDirectiveHook(
vnode: VNode,
prevVNode: VNode | null,
instance: ComponentInternalInstance | null,
name: keyof ObjectDirective,
): void {
const bindings = vnode.dirs!
const oldBindings = prevVNode && prevVNode.dirs!
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i]
if (oldBindings) {
binding.oldValue = oldBindings[i].value
}
let hook = binding.dir[name] as DirectiveHook | DirectiveHook[] | undefined
if (__COMPAT__ && !hook) {
hook = mapCompatDirectiveHook(name, binding.dir, instance)
}
if (hook) {
// disable tracking inside all lifecycle hooks
// since they can potentially be called inside effects.
pauseTracking()
callWithAsyncErrorHandling(hook, instance, ErrorCodes.DIRECTIVE_HOOK, [
vnode.el,
binding,
vnode,
prevVNode,
])
resetTracking()
}
}
}