mirror of https://github.com/vuejs/core.git
fix(defineModel): force local update when setter results in same emitted value
fix #10279 fix #10301
This commit is contained in:
parent
0ac0f2e338
commit
de174e1aa7
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
Fragment,
|
||||
type Ref,
|
||||
type TestElement,
|
||||
createApp,
|
||||
createBlock,
|
||||
createElementBlock,
|
||||
|
@ -526,4 +527,89 @@ describe('useModel', () => {
|
|||
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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
compatModelEmit,
|
||||
compatModelEventPrefix,
|
||||
} from './compat/componentVModel'
|
||||
import { getModelModifiers } from './helpers/useModel'
|
||||
|
||||
export type ObjectEmitsOptions = Record<
|
||||
string,
|
||||
|
@ -125,16 +126,12 @@ export function emit(
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,9 +29,13 @@ export function useModel(
|
|||
|
||||
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)) {
|
||||
|
@ -39,11 +43,13 @@ export function useModel(
|
|||
trigger()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
get() {
|
||||
track()
|
||||
return options.get ? options.get(localValue) : localValue
|
||||
},
|
||||
|
||||
set(value) {
|
||||
const rawProps = i.vnode!.props
|
||||
if (
|
||||
|
@ -59,24 +65,36 @@ export function useModel(
|
|||
) &&
|
||||
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 }
|
||||
}
|
||||
|
@ -86,3 +104,9 @@ export function useModel(
|
|||
|
||||
return res
|
||||
}
|
||||
|
||||
export const getModelModifiers = (
|
||||
props: Record<string, any>,
|
||||
modelName: string,
|
||||
): Record<string, boolean> | undefined =>
|
||||
props[`${modelName === 'modelValue' ? 'model' : modelName}Modifiers`]
|
||||
|
|
Loading…
Reference in New Issue