mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
feat(components): [input] add modelModifiers prop (#22415)
* feat(components): [input] add `modelModifiers` prop * refactor: update * refactor: update * fix: input uncontrolled in lazy mode * docs: add desc * perf: cache `Object.keys(props.modelModifiers).length` Co-authored-by: warmthsea <2586244885@qq.com> --------- Co-authored-by: warmthsea <2586244885@qq.com> Co-authored-by: btea <2356281422@qq.com>
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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(() => (
|
||||
<>
|
||||
<Input
|
||||
id="number"
|
||||
v-model={number.value}
|
||||
modelModifiers={{ number: true }}
|
||||
/>
|
||||
<Input id="trim" v-model={trim.value} modelModifiers={{ trim: true }} />
|
||||
<Input id="lazy" v-model={lazy.value} modelModifiers={{ lazy: true }} />
|
||||
<Input
|
||||
id="trim-number"
|
||||
v-model={trimNumber.value}
|
||||
modelModifiers={{ trim: true, number: true }}
|
||||
/>
|
||||
<Input
|
||||
id="trim-lazy"
|
||||
v-model={trimLazy.value}
|
||||
modelModifiers={{ trim: true, lazy: true }}
|
||||
/>
|
||||
<Input
|
||||
id="number-lazy"
|
||||
v-model={numberLazy.value}
|
||||
modelModifiers={{ number: true, lazy: true }}
|
||||
/>
|
||||
<Input
|
||||
id="trim-number-lazy"
|
||||
v-model={trimNumberLazy.value}
|
||||
modelModifiers={{ trim: true, number: true, lazy: true }}
|
||||
/>
|
||||
</>
|
||||
))
|
||||
|
||||
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
|
||||
// ...
|
||||
})
|
||||
|
||||
@@ -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<InputModelModifiers>(Object),
|
||||
default: () => ({}),
|
||||
},
|
||||
/**
|
||||
* @description same as `maxlength` in native input
|
||||
*/
|
||||
|
||||
@@ -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 <input> and <textarea>,
|
||||
// update DOM dependent value and styles
|
||||
|
||||
@@ -43,6 +43,11 @@ type TextAreaHeight = {
|
||||
minHeight?: string
|
||||
}
|
||||
|
||||
export const looseToNumber = (val: any): any => {
|
||||
const n = Number.parseFloat(val)
|
||||
return Number.isNaN(n) ? val : n
|
||||
}
|
||||
|
||||
function calculateNodeStyling(targetElement: Element): NodeStyle {
|
||||
const style = window.getComputedStyle(targetElement)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user