mirror of https://github.com/vuejs/core.git
124 lines
3.6 KiB
TypeScript
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`]
|
|
}
|