diff --git a/docs/en-US/component/input.md b/docs/en-US/component/input.md
index c132716fb6..79a543819a 100644
--- a/docs/en-US/component/input.md
+++ b/docs/en-US/component/input.md
@@ -121,6 +121,7 @@ input/length-limiting
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| type | type of input | ^[string]`'text' \| 'textarea' \| 'password' \| 'button' \| 'checkbox' \| 'file' \| 'number' \| 'radio' \| ...` [native input types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types) | text |
| model-value / v-model | binding value | ^[string] / ^[number] | — |
+| model-modifiers ^(2.11.5) | v-model modifiers, reference [Vue modifiers](https://vuejs.org/guide/essentials/forms.html#modifiers) | ^[object]`{ lazy? boolean, number?: boolean, trim?: boolean }` | — |
| maxlength | same as `maxlength` in native input | ^[string] / ^[number] | — |
| minlength | same as `minlength` in native input | ^[string] / ^[number] | — |
| show-word-limit | whether show word count, only works when `type` is 'text' or 'textarea' | ^[boolean] | false |
diff --git a/packages/components/input/__tests__/input.test.tsx b/packages/components/input/__tests__/input.test.tsx
index eff498b74a..17b41e89e3 100644
--- a/packages/components/input/__tests__/input.test.tsx
+++ b/packages/components/input/__tests__/input.test.tsx
@@ -289,12 +289,13 @@ describe('Input.vue', () => {
expect(vm.$el.querySelector('input').value).not.toEqual('1000')
vm.$el.querySelector('input').value = '1,000,000'
+ vm.$el.querySelector('input').dispatchEvent(event)
+ expect(val.value).toEqual('1000000')
+ expect(_val.value).toEqual('1000000')
+
vm.$el
.querySelector('input')
.dispatchEvent(new Event('change', { bubbles: true }))
- expect(_val.value).toEqual('1000000')
-
- vm.$el.querySelector('input').dispatchEvent(event)
expect(val.value).toEqual('1000000')
expect(_val.value).toEqual('1000000')
})
@@ -637,6 +638,128 @@ describe('Input.vue', () => {
})
})
+ test('modelValue modifiers', async () => {
+ const number = ref()
+ const trim = ref()
+ const lazy = ref()
+ const trimNumber = ref()
+ const trimLazy = ref()
+ const numberLazy = ref()
+ const trimNumberLazy = ref()
+
+ const wrapper = mount(() => (
+ <>
+
+
+
+
+
+
+
+ >
+ ))
+
+ await nextTick()
+
+ const triggerEvent = async (type: string, el: Element) => {
+ const event = new Event(type)
+ el.dispatchEvent(event)
+ await nextTick()
+ }
+ const mockActiveElement = vi.spyOn(document, 'activeElement', 'get')
+
+ const numberEl = wrapper.find('#number').element as HTMLInputElement
+ const trimEl = wrapper.find('#trim').element as HTMLInputElement
+ const lazyEl = wrapper.find('#lazy').element as HTMLInputElement
+ const trimNumberEl = wrapper.find('#trim-number')
+ .element as HTMLInputElement
+ const trimLazyEl = wrapper.find('#trim-lazy').element as HTMLInputElement
+ const numberLazyEl = wrapper.find('#number-lazy')
+ .element as HTMLInputElement
+ const trimNumberLazyEl = wrapper.find('#trim-number-lazy')
+ .element as HTMLInputElement
+
+ mockActiveElement.mockReturnValue(numberEl)
+ numberEl.value = '+01.2'
+ await triggerEvent('input', numberEl)
+ expect(number.value).toEqual(1.2)
+ expect(numberEl.value).toEqual('+01.2')
+ await triggerEvent('change', numberEl)
+ expect(numberEl.value).toEqual('1.2')
+
+ mockActiveElement.mockReturnValue(trimEl)
+ trimEl.value = ' hello, world '
+ await triggerEvent('input', trimEl)
+ expect(trim.value).toEqual('hello, world')
+ expect(trimEl.value).toEqual(' hello, world ')
+ await triggerEvent('change', trimEl)
+ expect(trimEl.value).toEqual('hello, world')
+
+ mockActiveElement.mockReturnValue(lazyEl)
+ lazyEl.value = 'foo'
+ await triggerEvent('input', lazyEl)
+ expect(lazy.value).toBeUndefined()
+ await triggerEvent('change', lazyEl)
+ expect(lazy.value).toEqual('foo')
+
+ mockActiveElement.mockReturnValue(trimNumberEl)
+ trimNumberEl.value = ' 1 '
+ await triggerEvent('input', trimNumberEl)
+ expect(trimNumber.value).toEqual(1)
+ expect(trimNumberEl.value).toEqual(' 1 ')
+ await triggerEvent('change', trimNumberEl)
+ expect(trimNumberEl.value).toEqual('1')
+
+ mockActiveElement.mockReturnValue(trimLazyEl)
+ trimLazyEl.value = ' hello, world '
+ await triggerEvent('input', trimLazyEl)
+ expect(trimLazy.value).toBeUndefined()
+ expect(trimLazyEl.value).toEqual(' hello, world ')
+ await triggerEvent('change', trimLazyEl)
+ expect(trimLazy.value).toEqual('hello, world')
+ expect(trimLazyEl.value).toEqual('hello, world')
+
+ mockActiveElement.mockReturnValue(numberLazyEl)
+ numberLazyEl.value = '+01.2'
+ await triggerEvent('input', numberLazyEl)
+ expect(numberLazy.value).toBeUndefined()
+ expect(numberLazyEl.value).toEqual('+01.2')
+ await triggerEvent('change', numberLazyEl)
+ expect(numberLazy.value).toEqual(1.2)
+ expect(numberLazyEl.value).toEqual('1.2')
+
+ mockActiveElement.mockReturnValue(trimNumberLazyEl)
+ trimNumberLazyEl.value = ' +01.2 '
+ await triggerEvent('input', trimNumberLazyEl)
+ expect(trimNumberLazy.value).toBeUndefined()
+ expect(trimNumberLazyEl.value).toEqual(' +01.2 ')
+ await triggerEvent('change', trimNumberLazyEl)
+ expect(trimNumberLazy.value).toEqual(1.2)
+ expect(trimNumberLazyEl.value).toEqual('1.2')
+
+ mockActiveElement.mockRestore()
+ })
+
// TODO: validateEvent & input containes select cases should be added after the rest components finished
// ...
})
diff --git a/packages/components/input/src/input.ts b/packages/components/input/src/input.ts
index 131a43911f..78bb13810a 100644
--- a/packages/components/input/src/input.ts
+++ b/packages/components/input/src/input.ts
@@ -16,6 +16,11 @@ import type {
__ExtractPublicPropTypes,
} from 'vue'
+export type InputModelModifiers = {
+ lazy?: boolean
+ number?: boolean
+ trim?: boolean
+}
export type InputAutoSize = { minRows?: number; maxRows?: number } | boolean
export const inputProps = buildProps({
@@ -45,6 +50,13 @@ export const inputProps = buildProps({
]),
default: '',
},
+ /**
+ * @description v-model modifiers, reference [Vue modifiers](https://vuejs.org/guide/essentials/forms.html#modifiers)
+ */
+ modelModifiers: {
+ type: definePropType(Object),
+ default: () => ({}),
+ },
/**
* @description same as `maxlength` in native input
*/
diff --git a/packages/components/input/src/input.vue b/packages/components/input/src/input.vue
index 705a8e5df6..ac0fc78887 100644
--- a/packages/components/input/src/input.vue
+++ b/packages/components/input/src/input.vue
@@ -189,7 +189,7 @@ import {
INPUT_EVENT,
UPDATE_MODEL_EVENT,
} from '@element-plus/constants'
-import { calcTextareaHeight } from './utils'
+import { calcTextareaHeight, looseToNumber } from './utils'
import { inputEmits, inputProps } from './input'
import type { StyleValue } from 'vue'
@@ -316,6 +316,9 @@ const suffixVisible = computed(
isWordLimitVisible.value ||
(!!validateState.value && needStatusIcon.value)
)
+const hasModelModifiers = computed(
+ () => !!Object.keys(props.modelModifiers).length
+)
const [recordCursor, setCursor] = useCursor(input)
@@ -384,43 +387,69 @@ const setNativeInputValue = () => {
input.value = formatterValue
}
-const handleInput = async (event: Event) => {
- recordCursor()
-
- let { value } = event.target as TargetElement
-
+const formatValue = (value: string) => {
+ const { trim, number } = props.modelModifiers
+ if (trim) {
+ value = value.trim()
+ }
+ if (number) {
+ value = `${looseToNumber(value)}`
+ }
if (props.formatter && props.parser) {
value = props.parser(value)
}
+ return value
+}
+const handleInput = async (event: Event) => {
// should not emit input during composition
// see: https://github.com/ElemeFE/element/issues/10516
if (isComposing.value) return
- // hack for https://github.com/ElemeFE/element/issues/8548
- // should remove the following line when we don't support IE
- if (value === nativeInputValue.value) {
- setNativeInputValue()
+ const { lazy } = props.modelModifiers
+ let { value } = event.target as TargetElement
+ if (lazy) {
+ emit(INPUT_EVENT, value)
return
}
+ value = formatValue(value)
+
+ // hack for https://github.com/ElemeFE/element/issues/8548
+ // should remove the following line when we don't support IE
+ if (String(value) === nativeInputValue.value) {
+ // preserve native features while being compatible with #9501
+ if (props.formatter) {
+ setNativeInputValue()
+ }
+ return
+ }
+
+ recordCursor()
emit(UPDATE_MODEL_EVENT, value)
emit(INPUT_EVENT, value)
// ensure native input value is controlled
// see: https://github.com/ElemeFE/element/issues/12850
await nextTick()
- setNativeInputValue()
+
+ if ((props.formatter && props.parser) || !hasModelModifiers.value) {
+ setNativeInputValue()
+ }
setCursor()
}
-const handleChange = (event: Event) => {
+const handleChange = async (event: Event) => {
let { value } = event.target as TargetElement
- if (props.formatter && props.parser) {
- value = props.parser(value)
+ value = formatValue(value)
+ if (props.modelModifiers.lazy) {
+ emit(UPDATE_MODEL_EVENT, value)
}
emit(CHANGE_EVENT, value)
+
+ await nextTick()
+ setNativeInputValue()
}
const {
@@ -479,7 +508,29 @@ watch(
// native input value is set explicitly
// do not use v-model / :value in template
// see: https://github.com/ElemeFE/element/issues/14521
-watch(nativeInputValue, () => setNativeInputValue())
+watch(nativeInputValue, (newValue) => {
+ if (!_ref.value) {
+ return
+ }
+ const { trim, number } = props.modelModifiers
+ const elValue = _ref.value.value
+ const displayValue =
+ (number || props.type === 'number') && !/^0\d/.test(elValue)
+ ? `${looseToNumber(elValue)}`
+ : elValue
+
+ if (displayValue === newValue) {
+ return
+ }
+
+ if (document.activeElement === _ref.value && _ref.value.type !== 'range') {
+ if (trim && displayValue.trim() === newValue) {
+ return
+ }
+ }
+
+ setNativeInputValue()
+})
// when change between and