import {
Fragment,
type Ref,
+ type TestElement,
createApp,
createBlock,
createElementBlock,
await nextTick()
expect(msg.value).toBe('UGHH')
})
+
+ // #10279
+ test('force local update when setter formats value to the same value', async () => {
+ let childMsg: Ref<string>
+ let childModifiers: Record<string, true | undefined>
+
+ const compRender = vi.fn()
+ const parentRender = vi.fn()
+
+ const Comp = defineComponent({
+ props: ['msg', 'msgModifiers'],
+ emits: ['update:msg'],
+ setup(props) {
+ ;[childMsg, childModifiers] = useModel(props, 'msg', {
+ set(val) {
+ if (childModifiers.number) {
+ return val.replace(/\D+/g, '')
+ }
+ },
+ })
+ return () => {
+ compRender()
+ return h('input', {
+ // simulate how v-model works
+ onVnodeBeforeMount(vnode) {
+ ;(vnode.el as TestElement).props.value = childMsg.value
+ },
+ onVnodeBeforeUpdate(vnode) {
+ ;(vnode.el as TestElement).props.value = childMsg.value
+ },
+ onInput(value: any) {
+ childMsg.value = value
+ },
+ })
+ }
+ },
+ })
+
+ const msg = ref(1)
+ const Parent = defineComponent({
+ setup() {
+ return () => {
+ parentRender()
+ return h(Comp, {
+ msg: msg.value,
+ msgModifiers: { number: true },
+ 'onUpdate:msg': val => {
+ msg.value = val
+ },
+ })
+ }
+ },
+ })
+
+ const root = nodeOps.createElement('div')
+ render(h(Parent), root)
+
+ expect(parentRender).toHaveBeenCalledTimes(1)
+ expect(compRender).toHaveBeenCalledTimes(1)
+ expect(serializeInner(root)).toBe('<input value=1></input>')
+
+ const input = root.children[0] as TestElement
+
+ // simulate v-model update
+ input.props.onInput((input.props.value = '2'))
+ await nextTick()
+ expect(msg.value).toBe(2)
+ expect(parentRender).toHaveBeenCalledTimes(2)
+ expect(compRender).toHaveBeenCalledTimes(2)
+ expect(serializeInner(root)).toBe('<input value=2></input>')
+
+ input.props.onInput((input.props.value = '2a'))
+ await nextTick()
+ expect(msg.value).toBe(2)
+ expect(parentRender).toHaveBeenCalledTimes(2)
+ // should force local update
+ expect(compRender).toHaveBeenCalledTimes(3)
+ expect(serializeInner(root)).toBe('<input value=2></input>')
+
+ input.props.onInput((input.props.value = '2a'))
+ await nextTick()
+ expect(parentRender).toHaveBeenCalledTimes(2)
+ // should not force local update if set to the same value
+ expect(compRender).toHaveBeenCalledTimes(3)
+ })
})
compatModelEmit,
compatModelEventPrefix,
} from './compat/componentVModel'
+import { getModelModifiers } from './helpers/useModel'
export type ObjectEmitsOptions = Record<
string,
const isModelListener = event.startsWith('update:')
// for v-model update:xxx events, apply modifiers on args
- const modelArg = isModelListener && event.slice(7)
- if (modelArg && modelArg in props) {
- const modifiersKey = `${
- modelArg === 'modelValue' ? 'model' : modelArg
- }Modifiers`
- const { number, trim } = props[modifiersKey] || EMPTY_OBJ
- if (trim) {
+ const modifiers = isModelListener && getModelModifiers(props, event.slice(7))
+ if (modifiers) {
+ if (modifiers.trim) {
args = rawArgs.map(a => (isString(a) ? a.trim() : a))
}
- if (number) {
+ if (modifiers.number) {
args = rawArgs.map(looseToNumber)
}
}
const camelizedName = camelize(name)
const hyphenatedName = hyphenate(name)
+ const modifiers = getModelModifiers(props, name)
const res = customRef((track, trigger) => {
let localValue: any
+ let prevSetValue: any
+ let prevEmittedValue: any
+
watchSyncEffect(() => {
const propValue = props[name]
if (hasChanged(localValue, propValue)) {
trigger()
}
})
+
return {
get() {
track()
return options.get ? options.get(localValue) : localValue
},
+
set(value) {
const rawProps = i.vnode!.props
if (
) &&
hasChanged(value, localValue)
) {
+ // no v-model, local update
localValue = value
trigger()
}
- i.emit(`update:${name}`, options.set ? options.set(value) : value)
+ const emittedValue = options.set ? options.set(value) : value
+ 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 (
+ value !== emittedValue &&
+ value !== prevSetValue &&
+ emittedValue === prevEmittedValue
+ ) {
+ trigger()
+ }
+ prevSetValue = value
+ prevEmittedValue = emittedValue
},
}
})
- const modifierKey =
- name === 'modelValue' ? 'modelModifiers' : `${name}Modifiers`
-
// @ts-expect-error
res[Symbol.iterator] = () => {
let i = 0
return {
next() {
if (i < 2) {
- return { value: i++ ? props[modifierKey] || {} : res, done: false }
+ 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 =>
+ props[`${modelName === 'modelValue' ? 'model' : modelName}Modifiers`]