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 {
|
import {
|
||||||
Fragment,
|
Fragment,
|
||||||
type Ref,
|
type Ref,
|
||||||
|
type TestElement,
|
||||||
createApp,
|
createApp,
|
||||||
createBlock,
|
createBlock,
|
||||||
createElementBlock,
|
createElementBlock,
|
||||||
|
@ -526,4 +527,89 @@ describe('useModel', () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(msg.value).toBe('UGHH')
|
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,
|
compatModelEmit,
|
||||||
compatModelEventPrefix,
|
compatModelEventPrefix,
|
||||||
} from './compat/componentVModel'
|
} from './compat/componentVModel'
|
||||||
|
import { getModelModifiers } from './helpers/useModel'
|
||||||
|
|
||||||
export type ObjectEmitsOptions = Record<
|
export type ObjectEmitsOptions = Record<
|
||||||
string,
|
string,
|
||||||
|
@ -125,16 +126,12 @@ export function emit(
|
||||||
const isModelListener = event.startsWith('update:')
|
const isModelListener = event.startsWith('update:')
|
||||||
|
|
||||||
// for v-model update:xxx events, apply modifiers on args
|
// for v-model update:xxx events, apply modifiers on args
|
||||||
const modelArg = isModelListener && event.slice(7)
|
const modifiers = isModelListener && getModelModifiers(props, event.slice(7))
|
||||||
if (modelArg && modelArg in props) {
|
if (modifiers) {
|
||||||
const modifiersKey = `${
|
if (modifiers.trim) {
|
||||||
modelArg === 'modelValue' ? 'model' : modelArg
|
|
||||||
}Modifiers`
|
|
||||||
const { number, trim } = props[modifiersKey] || EMPTY_OBJ
|
|
||||||
if (trim) {
|
|
||||||
args = rawArgs.map(a => (isString(a) ? a.trim() : a))
|
args = rawArgs.map(a => (isString(a) ? a.trim() : a))
|
||||||
}
|
}
|
||||||
if (number) {
|
if (modifiers.number) {
|
||||||
args = rawArgs.map(looseToNumber)
|
args = rawArgs.map(looseToNumber)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,9 +29,13 @@ export function useModel(
|
||||||
|
|
||||||
const camelizedName = camelize(name)
|
const camelizedName = camelize(name)
|
||||||
const hyphenatedName = hyphenate(name)
|
const hyphenatedName = hyphenate(name)
|
||||||
|
const modifiers = getModelModifiers(props, name)
|
||||||
|
|
||||||
const res = customRef((track, trigger) => {
|
const res = customRef((track, trigger) => {
|
||||||
let localValue: any
|
let localValue: any
|
||||||
|
let prevSetValue: any
|
||||||
|
let prevEmittedValue: any
|
||||||
|
|
||||||
watchSyncEffect(() => {
|
watchSyncEffect(() => {
|
||||||
const propValue = props[name]
|
const propValue = props[name]
|
||||||
if (hasChanged(localValue, propValue)) {
|
if (hasChanged(localValue, propValue)) {
|
||||||
|
@ -39,11 +43,13 @@ export function useModel(
|
||||||
trigger()
|
trigger()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get() {
|
get() {
|
||||||
track()
|
track()
|
||||||
return options.get ? options.get(localValue) : localValue
|
return options.get ? options.get(localValue) : localValue
|
||||||
},
|
},
|
||||||
|
|
||||||
set(value) {
|
set(value) {
|
||||||
const rawProps = i.vnode!.props
|
const rawProps = i.vnode!.props
|
||||||
if (
|
if (
|
||||||
|
@ -59,24 +65,36 @@ export function useModel(
|
||||||
) &&
|
) &&
|
||||||
hasChanged(value, localValue)
|
hasChanged(value, localValue)
|
||||||
) {
|
) {
|
||||||
|
// no v-model, local update
|
||||||
localValue = value
|
localValue = value
|
||||||
trigger()
|
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
|
// @ts-expect-error
|
||||||
res[Symbol.iterator] = () => {
|
res[Symbol.iterator] = () => {
|
||||||
let i = 0
|
let i = 0
|
||||||
return {
|
return {
|
||||||
next() {
|
next() {
|
||||||
if (i < 2) {
|
if (i < 2) {
|
||||||
return { value: i++ ? props[modifierKey] || {} : res, done: false }
|
return { value: i++ ? modifiers || EMPTY_OBJ : res, done: false }
|
||||||
} else {
|
} else {
|
||||||
return { done: true }
|
return { done: true }
|
||||||
}
|
}
|
||||||
|
@ -86,3 +104,9 @@ export function useModel(
|
||||||
|
|
||||||
return res
|
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