Skip to content

Commit de174e1

Browse files
committedJul 11, 2024··
fix(defineModel): force local update when setter results in same emitted value
fix #10279 fix #10301
1 parent 0ac0f2e commit de174e1

File tree

3 files changed

+120
-13
lines changed

3 files changed

+120
-13
lines changed
 

‎packages/runtime-core/__tests__/helpers/useModel.spec.ts

+86
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
Fragment,
33
type Ref,
4+
type TestElement,
45
createApp,
56
createBlock,
67
createElementBlock,
@@ -526,4 +527,89 @@ describe('useModel', () => {
526527
await nextTick()
527528
expect(msg.value).toBe('UGHH')
528529
})
530+
531+
// #10279
532+
test('force local update when setter formats value to the same value', async () => {
533+
let childMsg: Ref<string>
534+
let childModifiers: Record<string, true | undefined>
535+
536+
const compRender = vi.fn()
537+
const parentRender = vi.fn()
538+
539+
const Comp = defineComponent({
540+
props: ['msg', 'msgModifiers'],
541+
emits: ['update:msg'],
542+
setup(props) {
543+
;[childMsg, childModifiers] = useModel(props, 'msg', {
544+
set(val) {
545+
if (childModifiers.number) {
546+
return val.replace(/\D+/g, '')
547+
}
548+
},
549+
})
550+
return () => {
551+
compRender()
552+
return h('input', {
553+
// simulate how v-model works
554+
onVnodeBeforeMount(vnode) {
555+
;(vnode.el as TestElement).props.value = childMsg.value
556+
},
557+
onVnodeBeforeUpdate(vnode) {
558+
;(vnode.el as TestElement).props.value = childMsg.value
559+
},
560+
onInput(value: any) {
561+
childMsg.value = value
562+
},
563+
})
564+
}
565+
},
566+
})
567+
568+
const msg = ref(1)
569+
const Parent = defineComponent({
570+
setup() {
571+
return () => {
572+
parentRender()
573+
return h(Comp, {
574+
msg: msg.value,
575+
msgModifiers: { number: true },
576+
'onUpdate:msg': val => {
577+
msg.value = val
578+
},
579+
})
580+
}
581+
},
582+
})
583+
584+
const root = nodeOps.createElement('div')
585+
render(h(Parent), root)
586+
587+
expect(parentRender).toHaveBeenCalledTimes(1)
588+
expect(compRender).toHaveBeenCalledTimes(1)
589+
expect(serializeInner(root)).toBe('<input value=1></input>')
590+
591+
const input = root.children[0] as TestElement
592+
593+
// simulate v-model update
594+
input.props.onInput((input.props.value = '2'))
595+
await nextTick()
596+
expect(msg.value).toBe(2)
597+
expect(parentRender).toHaveBeenCalledTimes(2)
598+
expect(compRender).toHaveBeenCalledTimes(2)
599+
expect(serializeInner(root)).toBe('<input value=2></input>')
600+
601+
input.props.onInput((input.props.value = '2a'))
602+
await nextTick()
603+
expect(msg.value).toBe(2)
604+
expect(parentRender).toHaveBeenCalledTimes(2)
605+
// should force local update
606+
expect(compRender).toHaveBeenCalledTimes(3)
607+
expect(serializeInner(root)).toBe('<input value=2></input>')
608+
609+
input.props.onInput((input.props.value = '2a'))
610+
await nextTick()
611+
expect(parentRender).toHaveBeenCalledTimes(2)
612+
// should not force local update if set to the same value
613+
expect(compRender).toHaveBeenCalledTimes(3)
614+
})
529615
})

‎packages/runtime-core/src/componentEmits.ts

+5-8
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
compatModelEmit,
2929
compatModelEventPrefix,
3030
} from './compat/componentVModel'
31+
import { getModelModifiers } from './helpers/useModel'
3132

3233
export type ObjectEmitsOptions = Record<
3334
string,
@@ -125,16 +126,12 @@ export function emit(
125126
const isModelListener = event.startsWith('update:')
126127

127128
// for v-model update:xxx events, apply modifiers on args
128-
const modelArg = isModelListener && event.slice(7)
129-
if (modelArg && modelArg in props) {
130-
const modifiersKey = `${
131-
modelArg === 'modelValue' ? 'model' : modelArg
132-
}Modifiers`
133-
const { number, trim } = props[modifiersKey] || EMPTY_OBJ
134-
if (trim) {
129+
const modifiers = isModelListener && getModelModifiers(props, event.slice(7))
130+
if (modifiers) {
131+
if (modifiers.trim) {
135132
args = rawArgs.map(a => (isString(a) ? a.trim() : a))
136133
}
137-
if (number) {
134+
if (modifiers.number) {
138135
args = rawArgs.map(looseToNumber)
139136
}
140137
}

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

+29-5
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,27 @@ export function useModel(
2929

3030
const camelizedName = camelize(name)
3131
const hyphenatedName = hyphenate(name)
32+
const modifiers = getModelModifiers(props, name)
3233

3334
const res = customRef((track, trigger) => {
3435
let localValue: any
36+
let prevSetValue: any
37+
let prevEmittedValue: any
38+
3539
watchSyncEffect(() => {
3640
const propValue = props[name]
3741
if (hasChanged(localValue, propValue)) {
3842
localValue = propValue
3943
trigger()
4044
}
4145
})
46+
4247
return {
4348
get() {
4449
track()
4550
return options.get ? options.get(localValue) : localValue
4651
},
52+
4753
set(value) {
4854
const rawProps = i.vnode!.props
4955
if (
@@ -59,24 +65,36 @@ export function useModel(
5965
) &&
6066
hasChanged(value, localValue)
6167
) {
68+
// no v-model, local update
6269
localValue = value
6370
trigger()
6471
}
65-
i.emit(`update:${name}`, options.set ? options.set(value) : value)
72+
const emittedValue = options.set ? options.set(value) : value
73+
i.emit(`update:${name}`, emittedValue)
74+
// #10279: if the local value is converted via a setter but the value
75+
// emitted to parent was the same, the parent will not trigger any
76+
// updates and there will be no prop sync. However the local input state
77+
// may be out of sync, so we need to force an update here.
78+
if (
79+
value !== emittedValue &&
80+
value !== prevSetValue &&
81+
emittedValue === prevEmittedValue
82+
) {
83+
trigger()
84+
}
85+
prevSetValue = value
86+
prevEmittedValue = emittedValue
6687
},
6788
}
6889
})
6990

70-
const modifierKey =
71-
name === 'modelValue' ? 'modelModifiers' : `${name}Modifiers`
72-
7391
// @ts-expect-error
7492
res[Symbol.iterator] = () => {
7593
let i = 0
7694
return {
7795
next() {
7896
if (i < 2) {
79-
return { value: i++ ? props[modifierKey] || {} : res, done: false }
97+
return { value: i++ ? modifiers || EMPTY_OBJ : res, done: false }
8098
} else {
8199
return { done: true }
82100
}
@@ -86,3 +104,9 @@ export function useModel(
86104

87105
return res
88106
}
107+
108+
export const getModelModifiers = (
109+
props: Record<string, any>,
110+
modelName: string,
111+
): Record<string, boolean> | undefined =>
112+
props[`${modelName === 'modelValue' ? 'model' : modelName}Modifiers`]

0 commit comments

Comments
 (0)
Please sign in to comment.