vue3-core/packages/runtime-core/src/helpers/useModel.ts

124 lines
3.6 KiB
TypeScript

import { type Ref, customRef, ref } from '@vue/reactivity'
import { EMPTY_OBJ, camelize, hasChanged, hyphenate } from '@vue/shared'
import type { DefineModelOptions, ModelRef } from '../apiSetupHelpers'
import { getCurrentInstance } from '../component'
import { warn } from '../warning'
import type { NormalizedProps } from '../componentProps'
import { watchSyncEffect } from '../apiWatch'
export function useModel<
M extends PropertyKey,
T extends Record<string, any>,
K extends keyof T,
>(props: T, name: K, options?: DefineModelOptions<T[K]>): ModelRef<T[K], M>
export function useModel(
props: Record<string, any>,
name: string,
options: DefineModelOptions = EMPTY_OBJ,
): Ref {
const i = getCurrentInstance()!
if (__DEV__ && !i) {
warn(`useModel() called without active instance.`)
return ref() as any
}
if (__DEV__ && !(i.propsOptions[0] as NormalizedProps)[name]) {
warn(`useModel() called with prop "${name}" which is not declared.`)
return ref() as any
}
const camelizedName = camelize(name)
const hyphenatedName = hyphenate(name)
const modifiers = getModelModifiers(props, name)
const res = customRef((track, trigger) => {
let localValue: any
let prevSetValue: any = EMPTY_OBJ
let prevEmittedValue: any
watchSyncEffect(() => {
const propValue = props[name]
if (hasChanged(localValue, propValue)) {
localValue = propValue
trigger()
}
})
return {
get() {
track()
return options.get ? options.get(localValue) : localValue
},
set(value) {
const emittedValue = options.set ? options.set(value) : value
if (
!hasChanged(emittedValue, localValue) &&
!(prevSetValue !== EMPTY_OBJ && hasChanged(value, prevSetValue))
) {
return
}
const rawProps = i.vnode!.props
if (
!(
rawProps &&
// check if parent has passed v-model
(name in rawProps ||
camelizedName in rawProps ||
hyphenatedName in rawProps) &&
(`onUpdate:${name}` in rawProps ||
`onUpdate:${camelizedName}` in rawProps ||
`onUpdate:${hyphenatedName}` in rawProps)
)
) {
// no v-model, local update
localValue = value
trigger()
}
i.emit(`update:${name}`, emittedValue)
// #10279: if the local value is converted via a setter but the value
// emitted to parent was the same, the parent will not trigger any
// updates and there will be no prop sync. However the local input state
// may be out of sync, so we need to force an update here.
if (
hasChanged(value, emittedValue) &&
hasChanged(value, prevSetValue) &&
!hasChanged(emittedValue, prevEmittedValue)
) {
trigger()
}
prevSetValue = value
prevEmittedValue = emittedValue
},
}
})
// @ts-expect-error
res[Symbol.iterator] = () => {
let i = 0
return {
next() {
if (i < 2) {
return { value: i++ ? modifiers || EMPTY_OBJ : res, done: false }
} else {
return { done: true }
}
},
}
}
return res
}
export const getModelModifiers = (
props: Record<string, any>,
modelName: string,
): Record<string, boolean> | undefined => {
return modelName === 'modelValue' || modelName === 'model-value'
? props.modelModifiers
: props[`${modelName}Modifiers`] ||
props[`${camelize(modelName)}Modifiers`] ||
props[`${hyphenate(modelName)}Modifiers`]
}