diff --git a/src/runtime/components/forms/Input.vue b/src/runtime/components/forms/Input.vue index 8f77e2534b..94e856d4c4 100644 --- a/src/runtime/components/forms/Input.vue +++ b/src/runtime/components/forms/Input.vue @@ -14,6 +14,7 @@ v-bind="attrs" @input="onInput" @blur="onBlur" + @change="onChange" > @@ -36,9 +37,10 @@ import { ref, computed, toRef, onMounted, defineComponent } from 'vue' import type { PropType } from 'vue' import { twMerge, twJoin } from 'tailwind-merge' import UIcon from '../elements/Icon.vue' +import { defu } from 'defu' import { useUI } from '../../composables/useUI' import { useFormGroup } from '../../composables/useFormGroup' -import { mergeConfig } from '../../utils' +import { mergeConfig, looseToNumber } from '../../utils' import type { NestedKeyOf, Strategy } from '../../types' // @ts-expect-error import appConfig from '#build/app.config' @@ -156,6 +158,10 @@ export default defineComponent({ ui: { type: Object as PropType>, default: undefined + }, + modelModifiers: { + type: Object as PropType<{ trim?: boolean, lazy?: boolean, number?: boolean }>, + default: () => ({}) } }, emits: ['update:modelValue', 'blur'], @@ -164,6 +170,8 @@ export default defineComponent({ const { emitFormBlur, emitFormInput, size, color, inputId, name } = useFormGroup(props, config) + const modelModifiers = ref(defu({}, props.modelModifiers, { trim: false, lazy: false, number: false })) + const input = ref(null) const autoFocus = () => { @@ -172,11 +180,40 @@ export default defineComponent({ } } - const onInput = (event: InputEvent) => { - emit('update:modelValue', (event.target as HTMLInputElement).value) + // Custom function to handle the v-model properties + const updateInput = (value: string) => { + + if (modelModifiers.value.trim) { + value = value.trim() + } + + if (modelModifiers.value.number || props.type === 'number') { + value = looseToNumber(value) + } + + emit('update:modelValue', value) emitFormInput() } + const onInput = (event: InputEvent) => { + if (!modelModifiers.value.lazy) { + updateInput((event.target as HTMLInputElement).value) + } + } + + const onChange = (event: InputEvent) => { + const value = (event.target as HTMLInputElement).value + + if (modelModifiers.value.lazy) { + updateInput(value) + } + + // Update trimmed input so that it has same behaviour as native input https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63 + if (modelModifiers.value.trim) { + (event.target as HTMLInputElement).value = value.trim() + } + } + const onBlur = (event: FocusEvent) => { emitFormBlur() emit('blur', event) @@ -280,6 +317,7 @@ export default defineComponent({ trailingIconClass, trailingWrapperIconClass, onInput, + onChange, onBlur } } diff --git a/src/runtime/components/forms/Textarea.vue b/src/runtime/components/forms/Textarea.vue index 54cc838b64..d3dff6a31e 100644 --- a/src/runtime/components/forms/Textarea.vue +++ b/src/runtime/components/forms/Textarea.vue @@ -14,6 +14,7 @@ v-bind="attrs" @input="onInput" @blur="onBlur" + @change="onChange" /> @@ -22,9 +23,10 @@ import { ref, computed, toRef, watch, onMounted, nextTick, defineComponent } from 'vue' import type { PropType } from 'vue' import { twMerge, twJoin } from 'tailwind-merge' +import { defu } from 'defu' import { useUI } from '../../composables/useUI' import { useFormGroup } from '../../composables/useFormGroup' -import { mergeConfig } from '../../utils' +import { mergeConfig, looseToNumber } from '../../utils' import type { NestedKeyOf, Strategy } from '../../types' // @ts-expect-error import appConfig from '#build/app.config' @@ -119,6 +121,10 @@ export default defineComponent({ ui: { type: Object as PropType>, default: undefined + }, + modelModifiers: { + type: Object as PropType<{ trim?: boolean, lazy?: boolean, number?: boolean }>, + default: () => ({}) } }, emits: ['update:modelValue', 'blur'], @@ -127,6 +133,8 @@ export default defineComponent({ const { emitFormBlur, emitFormInput, inputId, color, size, name } = useFormGroup(props, config) + const modelModifiers = ref(defu({}, props.modelModifiers, { trim: false, lazy: false, number: false })) + const textarea = ref(null) const autoFocus = () => { @@ -157,11 +165,38 @@ export default defineComponent({ } } + // Custom function to handle the v-model properties + const updateInput = (value: string) => { + if (modelModifiers.value.trim) { + value = value.trim() + } + + if (modelModifiers.value.number) { + value = looseToNumber(value) + } + + emit('update:modelValue', value) + emitFormInput() + } + const onInput = (event: InputEvent) => { autoResize() + if (!modelModifiers.value.lazy) { + updateInput((event.target as HTMLInputElement).value) + } + } - emit('update:modelValue', (event.target as HTMLInputElement).value) - emitFormInput() + const onChange = (event: InputEvent) => { + const value = (event.target as HTMLInputElement).value + + if (modelModifiers.value.lazy) { + updateInput(value) + } + + // Update trimmed input so that it has same behaviour as native input + if (modelModifiers.value.trim) { + (event.target as HTMLInputElement).value = value.trim() + } } const onBlur = (event: FocusEvent) => { @@ -211,6 +246,7 @@ export default defineComponent({ // eslint-disable-next-line vue/no-dupe-keys textareaClass, onInput, + onChange, onBlur } } diff --git a/src/runtime/utils/index.ts b/src/runtime/utils/index.ts index 9e3e0d0746..1af846261f 100644 --- a/src/runtime/utils/index.ts +++ b/src/runtime/utils/index.ts @@ -56,4 +56,13 @@ export function getSlotsChildren (slots: any) { return children } +/** + * "123-foo" will be parsed to 123 + * This is used for the .number modifier in v-model + */ +export function looseToNumber (val: any): any { + const n = parseFloat(val) + return isNaN(n) ? val : n +} + export * from './lodash'