Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 29af703

Browse files
committedMar 18, 2025
fix(NumberField/NumberFieldRoot): emit update:modelValue events after child input event (unovue#1720)
The input event fires more frequently, after each keypress than the currently used change event. Creates consistency with how a native number input would integrate with v-model.
1 parent 59bda43 commit 29af703

File tree

4 files changed

+50
-5
lines changed

4 files changed

+50
-5
lines changed
 

‎packages/core/src/NumberField/NumberField.test.ts

+23
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,29 @@ describe('numberField', () => {
263263
await fireEvent.keyDown(input, { key: kbd.ENTER })
264264
expect(input.value).toBe('EUR 7.00')
265265
})
266+
267+
it('should correctly update model value independently of formatting', async () => {
268+
const { input, emitted } = setup({
269+
defaultValue: 100,
270+
})
271+
272+
// after update, both internal and displayed value are not formatted
273+
await fireEvent.update(input, '1000')
274+
expect(input.value).toBe('1000')
275+
expect(emitted('update:model-value')[0]).toEqual([1000])
276+
// update:model-value fired after each consecutive update
277+
await fireEvent.update(input, '10000')
278+
expect(input.value).toBe('10000')
279+
expect(emitted('update:model-value')[1]).toEqual([10000])
280+
// on 'enter' key press formatting is applied to text, model value does not change
281+
await fireEvent.keyDown(input, { key: kbd.ENTER })
282+
expect(input.value).toBe('10,000')
283+
expect(emitted('update:model-value').length).toBe(2)
284+
// update:model-value always sends non-formatted value even if current text is formatted
285+
await fireEvent.update(input, '10,001')
286+
expect(input.value).toBe('10,001')
287+
expect(emitted('update:model-value')[2]).toEqual([10001])
288+
})
266289
})
267290
})
268291

‎packages/core/src/NumberField/NumberFieldInput.vue

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ function handleChange() {
9393
@input="(event: InputEvent) => {
9494
const target = event.target as HTMLInputElement
9595
inputValue = target.value
96+
rootContext.applyInputValue(target.value, false)
9697
}"
9798
@change="handleChange"
9899
@keydown.enter="rootContext.applyInputValue($event.target?.value)"

‎packages/core/src/NumberField/NumberFieldRoot.vue

+24-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import type { PrimitiveProps } from '@/Primitive'
33
import { useVModel } from '@vueuse/core'
44
import { clamp, createContext, snapValueToStep, useFormControl, useLocale } from '@/shared'
5-
import { type HTMLAttributes, type Ref, computed, ref, toRefs } from 'vue'
5+
import { type HTMLAttributes, type Ref, computed, ref, toRefs, watch } from 'vue'
66
import type { FormFieldProps } from '@/shared/types'
77
88
export interface NumberFieldRootProps extends PrimitiveProps, FormFieldProps {
@@ -40,7 +40,7 @@ interface NumberFieldRootContext {
4040
inputMode: Ref<HTMLAttributes['inputmode']>
4141
textValue: Ref<string>
4242
validate: (val: string) => boolean
43-
applyInputValue: (val: string) => void
43+
applyInputValue: (val: string, format?: boolean) => void
4444
disabled: Ref<boolean>
4545
max: Ref<number | undefined>
4646
min: Ref<number | undefined>
@@ -68,7 +68,7 @@ const props = withDefaults(defineProps<NumberFieldRootProps>(), {
6868
stepSnapping: true,
6969
})
7070
const emits = defineEmits<NumberFieldRootEmits>()
71-
const { disabled, min, max, step, stepSnapping, formatOptions, id, locale: propLocale } = toRefs(props)
71+
const { disabled, defaultValue, min, max, step, stepSnapping, formatOptions, id, locale: propLocale } = toRefs(props)
7272
7373
const modelValue = useVModel(props, 'modelValue', emits, {
7474
defaultValue: props.defaultValue,
@@ -80,6 +80,8 @@ const { primitiveElement, currentElement } = usePrimitiveElement()
8080
const locale = useLocale(propLocale)
8181
const isFormControl = useFormControl(currentElement)
8282
const inputEl = ref<HTMLInputElement>()
83+
const textValue = ref<string>('')
84+
let skipFormatNextUpdate: boolean = false
8385
8486
const isDecreaseDisabled = computed(() => (
8587
clampInputValue(modelValue.value) === min.value
@@ -136,7 +138,18 @@ const inputMode = computed<HTMLAttributes['inputmode']>(() => {
136138
// Replace negative textValue formatted using currencySign: 'accounting'
137139
// with a textValue that can be announced using a minus sign.
138140
const textValueFormatter = useNumberFormatter(locale, formatOptions)
139-
const textValue = computed(() => isNaN(modelValue.value) ? '' : textValueFormatter.format(modelValue.value))
141+
142+
updateTextValue(modelValue.value)
143+
144+
watch([modelValue, defaultValue, formatOptions, propLocale], () => {
145+
if (!skipFormatNextUpdate)
146+
updateTextValue(modelValue.value)
147+
skipFormatNextUpdate = false
148+
})
149+
150+
function updateTextValue(value: number | null) {
151+
textValue.value = (value === null || isNaN(value)) ? '' : textValueFormatter.format(value)
152+
}
140153
141154
function validate(val: string) {
142155
return numberParser.isValidPartialNumber(val, min.value, max.value)
@@ -159,10 +172,16 @@ function clampInputValue(val: number) {
159172
return clampedValue
160173
}
161174
162-
function applyInputValue(val: string) {
175+
function applyInputValue(val: string, format: boolean = true) {
163176
const parsedValue = numberParser.parse(val)
164177
178+
// if formatting not requested, set flag to skip formatting in the watcher before updating the value
179+
if (!format)
180+
skipFormatNextUpdate = true
165181
modelValue.value = clampInputValue(parsedValue)
182+
// if formatting is requested, force a text value update here in case the current value is the same as previous
183+
if (format)
184+
updateTextValue(modelValue.value)
166185
// Set to empty state if input value is empty
167186
if (!val.length)
168187
return setInputValue(val)

‎packages/core/src/NumberField/story/_NumberField.vue

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { NumberFieldDecrement, NumberFieldIncrement, NumberFieldInput, NumberFie
44
import { Icon } from '@iconify/vue'
55
66
const props = defineProps<NumberFieldRootProps>()
7+
const emit = defineEmits(['update:model-value'])
78
</script>
89

910
<template>
@@ -12,6 +13,7 @@ const props = defineProps<NumberFieldRootProps>()
1213
id="number-field"
1314
data-testid="root"
1415
class="text-sm flex items-center border bg-blackA7 border-blackA9 rounded-md text-white"
16+
@update:model-value="emit('update:model-value', $event)"
1517
>
1618
<label
1719
for="number-field"

0 commit comments

Comments
 (0)
Please sign in to comment.