import { isBuiltInDirective } from '@vue/shared'
-import { type ComponentInternalInstance, currentInstance } from './component'
+import {
+ type ComponentInternalInstance,
+ currentInstance,
+ isVaporComponent,
+} from './component'
import { warn } from './warning'
+import { normalizeBlock } from './dom/element'
+import { getCurrentScope } from '@vue/reactivity'
+import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
export type DirectiveModifiers<M extends string = string> = Record<M, boolean>
export interface DirectiveBinding<T = any, V = any, M extends string = string> {
instance: ComponentInternalInstance
- source?: () => V
- value: V
- oldValue: V | null
+ source: () => V
arg?: string
modifiers?: DirectiveModifiers<M>
- dir: ObjectDirective<T, V, M>
+ dir: Directive<T, V, M>
}
export type DirectiveBindingsMap = Map<Node, DirectiveBinding[]>
-export type DirectiveHook<
- T = any | null,
- V = any,
- M extends string = string,
-> = (node: T, binding: DirectiveBinding<T, V, M>) => void
-
-// create node -> `created` -> node operation -> `beforeMount` -> node mounted -> `mounted`
-// effect update -> `beforeUpdate` -> node updated -> `updated`
-// `beforeUnmount`-> node unmount -> `unmounted`
-export type DirectiveHookName =
- | 'created'
- | 'beforeMount'
- | 'mounted'
- | 'beforeUpdate'
- | 'updated'
- | 'beforeUnmount'
- | 'unmounted'
-export type ObjectDirective<T = any, V = any, M extends string = string> = {
- [K in DirectiveHookName]?: DirectiveHook<T, V, M> | undefined
-} & {
- /** Watch value deeply */
- deep?: boolean | number
-}
-
-export type FunctionDirective<
- T = any,
- V = any,
- M extends string = string,
-> = DirectiveHook<T, V, M>
-
-export type Directive<T = any, V = any, M extends string = string> =
- | ObjectDirective<T, V, M>
- | FunctionDirective<T, V, M>
+export type Directive<T = any, V = any, M extends string = string> = (
+ node: T,
+ binding: DirectiveBinding<T, V, M>,
+) => void
export function validateDirectiveName(name: string): void {
if (isBuiltInDirective(name)) {
return nodeOrComponent
}
- // NOOP
+ let node: Node
+ if (isVaporComponent(nodeOrComponent)) {
+ const root = getComponentNode(nodeOrComponent)
+ if (!root) return nodeOrComponent
+ node = root
+ } else {
+ node = nodeOrComponent
+ }
+
+ const instance = currentInstance!
+ const parentScope = getCurrentScope()
+
+ if (__DEV__ && !parentScope) {
+ warn(`Directives should be used inside of RenderEffectScope.`)
+ }
+
+ for (const directive of directives) {
+ let [dir, source = () => undefined, arg, modifiers] = directive
+ if (!dir) continue
+
+ const binding: DirectiveBinding = {
+ dir,
+ source,
+ instance,
+ arg,
+ modifiers,
+ }
+
+ callWithAsyncErrorHandling(dir, instance, VaporErrorCodes.DIRECTIVE_HOOK, [
+ node,
+ binding,
+ ])
+ }
return nodeOrComponent
}
+
+function getComponentNode(component: ComponentInternalInstance) {
+ if (!component.block) return
+
+ const nodes = normalizeBlock(component.block)
+ if (nodes.length !== 1) {
+ warn(
+ `Runtime directive used on component with non-element root node. ` +
+ `The directives will not function as intended.`,
+ )
+ return
+ }
+
+ return nodes[0]
+}
looseIndexOf,
looseToNumber,
} from '@vue/shared'
-import type {
- DirectiveBinding,
- DirectiveHook,
- DirectiveHookName,
- ObjectDirective,
-} from '../directives'
+import type { Directive } from '../directives'
import { addEventListener } from '../dom/event'
import { nextTick } from '../scheduler'
import { warn } from '../warning'
import { MetadataKind, getMetadata } from '../componentMetadata'
+import {
+ onBeforeMount,
+ onBeforeUnmount,
+ onBeforeUpdate,
+ onMounted,
+} from '../apiLifecycle'
+import { renderEffect } from '../renderEffect'
type AssignerFn = (value: any) => void
function getModelAssigner(el: Element): AssignerFn {
// 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: ObjectDirective<
+export const vModelText: Directive<
HTMLInputElement | HTMLTextAreaElement,
any,
'lazy' | 'trim' | 'number'
-> = {
- beforeMount(el, { modifiers: { lazy, trim, number } = {} }) {
+> = (el, { source, modifiers: { lazy, trim, number } = {} }) => {
+ onBeforeMount(() => {
const assigner = getModelAssigner(el)
assignFnMap.set(el, assigner)
// fires "change" instead of "input" on autocomplete.
addEventListener(el, 'change', onCompositionEnd)
}
- },
- // set value on mounted so it's after min/max for type="range"
- mounted(el, { value }) {
+ })
+
+ onMounted(() => {
+ const value = source()
el.value = value == null ? '' : value
- },
- beforeUpdate(el, { value, modifiers: { lazy, trim, number } = {} }) {
+ })
+
+ renderEffect(() => {
+ const value = source()
assignFnMap.set(el, getModelAssigner(el))
// avoid clearing unresolved text. #2302
}
el.value = newValue
- },
+ })
}
-export const vModelRadio: ObjectDirective<HTMLInputElement> = {
- beforeMount(el, { value }) {
- el.checked = looseEqual(value, getValue(el))
+export const vModelRadio: Directive<HTMLInputElement> = (el, { source }) => {
+ onBeforeMount(() => {
+ el.checked = looseEqual(source(), getValue(el))
assignFnMap.set(el, getModelAssigner(el))
addEventListener(el, 'change', () => {
assignFnMap.get(el)!(getValue(el))
})
- },
- beforeUpdate(el, { value, oldValue }) {
+ })
+
+ renderEffect(() => {
+ const value = source()
assignFnMap.set(el, getModelAssigner(el))
- if (value !== oldValue) {
- el.checked = looseEqual(value, getValue(el))
- }
- },
+ el.checked = looseEqual(value, getValue(el))
+ })
}
-export const vModelSelect: ObjectDirective<HTMLSelectElement, any, 'number'> = {
- // <select multiple> value need to be deep traversed
- deep: true,
- beforeMount(el, { value, modifiers: { number = false } = {} }) {
+export const vModelSelect: Directive<HTMLSelectElement, any, 'number'> = (
+ el,
+ { source, modifiers: { number = false } = {} },
+) => {
+ onBeforeMount(() => {
+ const value = source()
const isSetModel = isSet(value)
addEventListener(el, 'change', () => {
const selectedVal = Array.prototype.filter
})
assignFnMap.set(el, getModelAssigner(el))
setSelected(el, value, number)
- },
- beforeUpdate(el) {
+ })
+
+ onBeforeUnmount(() => {
assignFnMap.set(el, getModelAssigner(el))
- },
- updated(el, { value, modifiers: { number = false } = {} }) {
+ })
+
+ renderEffect(() => {
if (!assigningMap.get(el)) {
- setSelected(el, value, number)
+ setSelected(el, source(), number)
}
- },
+ })
}
function setSelected(el: HTMLSelectElement, value: any, number: boolean) {
return checked
}
-const setChecked: DirectiveHook<HTMLInputElement> = (
- el,
- { value, oldValue },
-) => {
- if (isArray(value)) {
- el.checked = looseIndexOf(value, getValue(el)) > -1
- } else if (isSet(value)) {
- el.checked = value.has(getValue(el))
- } else if (value !== oldValue) {
- el.checked = looseEqual(value, getCheckboxValue(el, true))
- }
-}
-
-export const vModelCheckbox: ObjectDirective<HTMLInputElement> = {
- // #4096 array checkboxes need to be deep traversed
- deep: true,
- beforeMount(el, binding) {
+export const vModelCheckbox: Directive<HTMLInputElement> = (el, { source }) => {
+ onBeforeMount(() => {
assignFnMap.set(el, getModelAssigner(el))
addEventListener(el, 'change', () => {
- const modelValue = binding.value
+ const modelValue = source()
const elementValue = getValue(el)
const checked = el.checked
const assigner = assignFnMap.get(el)!
assigner(getCheckboxValue(el, checked))
}
})
- },
- // set initial checked on mount to wait for true-value/false-value
- mounted: setChecked,
- beforeUpdate(el, binding) {
+ })
+
+ onMounted(() => {
+ setChecked()
+ })
+
+ onBeforeUpdate(() => {
assignFnMap.set(el, getModelAssigner(el))
- setChecked(el, binding)
- },
+ setChecked()
+ })
+
+ function setChecked() {
+ const value = source()
+ if (isArray(value)) {
+ el.checked = looseIndexOf(value, getValue(el)) > -1
+ } else if (isSet(value)) {
+ el.checked = value.has(getValue(el))
+ } else {
+ el.checked = looseEqual(value, getCheckboxValue(el, true))
+ }
+ }
}
-export const vModelDynamic: ObjectDirective<
+export const vModelDynamic: Directive<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
-> = {
- beforeMount(el, binding) {
- callModelHook(el, binding, 'beforeMount')
- },
- mounted(el, binding) {
- callModelHook(el, binding, 'mounted')
- },
- beforeUpdate(el, binding) {
- callModelHook(el, binding, 'beforeUpdate')
- },
- updated(el, binding) {
- callModelHook(el, binding, 'updated')
- },
+> = (el, binding) => {
+ const type = el.getAttribute('type')
+ const modelToUse = resolveDynamicModel(el.tagName, type)
+ modelToUse(el, binding)
}
-function resolveDynamicModel(
- tagName: string,
- type: string | null,
-): ObjectDirective {
+function resolveDynamicModel(tagName: string, type: string | null): Directive {
switch (tagName) {
case 'SELECT':
return vModelSelect
}
}
}
-
-function callModelHook(
- el: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement,
- binding: DirectiveBinding,
- hook: DirectiveHookName,
-) {
- const type = el.getAttribute('type')
- const modelToUse = resolveDynamicModel(el.tagName, type)
- const fn = modelToUse[hook]
- fn && fn(el, binding)
-}
-import type { ObjectDirective } from '../directives'
+import type { Directive } from '../directives'
+import { renderEffect } from '../renderEffect'
-const vShowMap = new WeakMap<HTMLElement, string>()
+export const vShowOriginalDisplay: unique symbol = Symbol('_vod')
+export const vShowHidden: unique symbol = Symbol('_vsh')
-export const vShow: ObjectDirective<HTMLElement> = {
- beforeMount(node, { value }) {
- vShowMap.set(node, node.style.display === 'none' ? '' : node.style.display)
- setDisplay(node, value)
- },
-
- updated(node, { value, oldValue }) {
- if (!value === !oldValue) return
- setDisplay(node, value)
- },
+export interface VShowElement extends HTMLElement {
+ // _vod = vue original display
+ [vShowOriginalDisplay]: string
+ [vShowHidden]: boolean
+}
- beforeUnmount(node, { value }) {
- setDisplay(node, value)
- },
+export const vShow: Directive<VShowElement> = (el, { source }) => {
+ el[vShowOriginalDisplay] = el.style.display === 'none' ? '' : el.style.display
+ renderEffect(() => setDisplay(el, source()))
}
-function setDisplay(el: HTMLElement, value: unknown): void {
- el.style.display = value ? vShowMap.get(el)! : 'none'
+function setDisplay(el: VShowElement, value: unknown): void {
+ el.style.display = value ? el[vShowOriginalDisplay] : 'none'
+ el[vShowHidden] = !value
}