mirror of
https://github.com/element-plus/element-plus.git
synced 2024-11-27 02:01:15 +08:00
feat(components): [input-number] add :value-on-clear and make it nullable (#7724)
This commit is contained in:
parent
9ae57642db
commit
2577b06328
@ -81,21 +81,22 @@ input-number/controlled
|
||||
|
||||
## Attributes
|
||||
|
||||
| Attribute | Description | Type | Accepted Values | Default |
|
||||
| --------------------- | ------------------------------------------------ | ------------------ | --------------- | ----------- |
|
||||
| model-value / v-model | binding value | number / undefined | — | — |
|
||||
| min | the minimum allowed value | number | — | `-Infinity` |
|
||||
| max | the maximum allowed value | number | — | `Infinity` |
|
||||
| step | incremental step | number | — | 1 |
|
||||
| step-strictly | whether input value can only be multiple of step | boolean | — | false |
|
||||
| precision | precision of input value | number | — | — |
|
||||
| size | size of the component | string | large/small | default |
|
||||
| disabled | whether the component is disabled | boolean | — | false |
|
||||
| controls | whether to enable the control buttons | boolean | — | true |
|
||||
| controls-position | position of the control buttons | string | right | - |
|
||||
| name | same as `name` in native input | string | — | — |
|
||||
| label | label text | string | — | — |
|
||||
| placeholder | placeholder in input | string | - | - |
|
||||
| Attribute | Description | Type | Accepted Values | Default |
|
||||
| --------------------- | ------------------------------------------------ | ---------------------- | --------------- | ----------- |
|
||||
| model-value / v-model | binding value | number / undefined | — | — |
|
||||
| min | the minimum allowed value | number | — | `-Infinity` |
|
||||
| max | the maximum allowed value | number | — | `Infinity` |
|
||||
| step | incremental step | number | — | 1 |
|
||||
| step-strictly | whether input value can only be multiple of step | boolean | — | false |
|
||||
| precision | precision of input value | number | — | — |
|
||||
| size | size of the component | string | large/small | default |
|
||||
| disabled | whether the component is disabled | boolean | — | false |
|
||||
| controls | whether to enable the control buttons | boolean | — | true |
|
||||
| controls-position | position of the control buttons | string | right | - |
|
||||
| name | same as `name` in native input | string | — | — |
|
||||
| label | label text | string | — | — |
|
||||
| placeholder | placeholder in input | string | - | - |
|
||||
| value-on-clear | value should be set when input box is cleared | string / number / null | min/max | - |
|
||||
|
||||
## Events
|
||||
|
||||
|
@ -74,7 +74,7 @@ describe('InputNumber.vue', () => {
|
||||
await nextTick()
|
||||
expect(wrapper.find('input').element.value).toEqual('')
|
||||
expect(wrapper.find('input').element.getAttribute('aria-valuenow')).toEqual(
|
||||
'NaN'
|
||||
'null'
|
||||
)
|
||||
})
|
||||
test('min', async () => {
|
||||
@ -278,9 +278,41 @@ describe('InputNumber.vue', () => {
|
||||
expect(wrapper.getComponent(InputNumber).emitted('focus')).toHaveLength(1)
|
||||
})
|
||||
|
||||
test('clear', async () => {
|
||||
test('clear with :value-on-clear="null"', async () => {
|
||||
const wrapper = _mount({
|
||||
template: '<el-input-number v-model="num" :min="1"/>',
|
||||
template: '<el-input-number v-model="num" :min="1" :max="10"/>',
|
||||
setup() {
|
||||
const num = ref(2)
|
||||
return {
|
||||
num,
|
||||
}
|
||||
},
|
||||
})
|
||||
const elInput = wrapper.findComponent({ name: 'ElInputNumber' }).vm
|
||||
elInput.handleInputChange('')
|
||||
await nextTick()
|
||||
expect(wrapper.vm.num).toBe(null)
|
||||
elInput.increase()
|
||||
await nextTick()
|
||||
expect(wrapper.vm.num).toBe(1)
|
||||
elInput.increase()
|
||||
await nextTick()
|
||||
expect(wrapper.vm.num).toBe(2)
|
||||
elInput.handleInputChange('')
|
||||
await nextTick()
|
||||
expect(wrapper.vm.num).toBe(null)
|
||||
elInput.decrease()
|
||||
await nextTick()
|
||||
expect(wrapper.vm.num).toBe(1)
|
||||
elInput.decrease()
|
||||
await nextTick()
|
||||
expect(wrapper.vm.num).toBe(1)
|
||||
})
|
||||
|
||||
test('clear with value-on-clear="min"', async () => {
|
||||
const wrapper = _mount({
|
||||
template:
|
||||
'<el-input-number v-model="num" value-on-clear="min" :min="1" :max="10"/>',
|
||||
setup() {
|
||||
const num = ref(2)
|
||||
return {
|
||||
@ -303,6 +335,58 @@ describe('InputNumber.vue', () => {
|
||||
expect(wrapper.vm.num).toBe(1)
|
||||
})
|
||||
|
||||
test('clear with value-on-clear="max"', async () => {
|
||||
const wrapper = _mount({
|
||||
template:
|
||||
'<el-input-number v-model="num" value-on-clear="max" :min="1" :max="10"/>',
|
||||
setup() {
|
||||
const num = ref(2)
|
||||
return {
|
||||
num,
|
||||
}
|
||||
},
|
||||
})
|
||||
const elInput = wrapper.findComponent({ name: 'ElInputNumber' }).vm
|
||||
elInput.handleInputChange('')
|
||||
await nextTick()
|
||||
expect(wrapper.vm.num).toBe(10)
|
||||
elInput.increase()
|
||||
await nextTick()
|
||||
expect(wrapper.vm.num).toBe(10)
|
||||
elInput.handleInputChange('')
|
||||
await nextTick()
|
||||
expect(wrapper.vm.num).toBe(10)
|
||||
elInput.decrease()
|
||||
await nextTick()
|
||||
expect(wrapper.vm.num).toBe(9)
|
||||
})
|
||||
|
||||
test('clear with :value-on-clear="5"', async () => {
|
||||
const wrapper = _mount({
|
||||
template:
|
||||
'<el-input-number v-model="num" :value-on-clear="5" :min="1" :max="10"/>',
|
||||
setup() {
|
||||
const num = ref(2)
|
||||
return {
|
||||
num,
|
||||
}
|
||||
},
|
||||
})
|
||||
const elInput = wrapper.findComponent({ name: 'ElInputNumber' }).vm
|
||||
elInput.handleInputChange('')
|
||||
await nextTick()
|
||||
expect(wrapper.vm.num).toBe(5)
|
||||
elInput.increase()
|
||||
await nextTick()
|
||||
expect(wrapper.vm.num).toBe(6)
|
||||
elInput.handleInputChange('')
|
||||
await nextTick()
|
||||
expect(wrapper.vm.num).toBe(5)
|
||||
elInput.decrease()
|
||||
await nextTick()
|
||||
expect(wrapper.vm.num).toBe(4)
|
||||
})
|
||||
|
||||
test('check increase and decrease button when modelValue not in [min, max]', async () => {
|
||||
const wrapper = _mount({
|
||||
template: `
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { isNil } from 'lodash-unified'
|
||||
import { buildProps, isNumber } from '@element-plus/utils'
|
||||
import { componentSizes } from '@element-plus/constants'
|
||||
|
||||
@ -42,6 +43,12 @@ export const inputNumberProps = buildProps({
|
||||
default: '',
|
||||
values: ['', 'right'],
|
||||
},
|
||||
valueOnClear: {
|
||||
type: [String, Number, null],
|
||||
validator: (val: 'min' | 'max' | number | null) =>
|
||||
val === null || isNumber(val) || ['min', 'max'].includes(val),
|
||||
default: null,
|
||||
},
|
||||
name: String,
|
||||
label: String,
|
||||
placeholder: String,
|
||||
@ -56,7 +63,6 @@ export const inputNumberEmits = {
|
||||
change: (prev: number | undefined, cur: number | undefined) => prev !== cur,
|
||||
blur: (e: FocusEvent) => e instanceof FocusEvent,
|
||||
focus: (e: FocusEvent) => e instanceof FocusEvent,
|
||||
input: (val: number | undefined) => isNumber(val),
|
||||
'update:modelValue': (val: number | undefined) =>
|
||||
isNumber(val) || val === undefined,
|
||||
input: (val: number | null | undefined) => isNumber(val) || isNil(val),
|
||||
'update:modelValue': (val: number | undefined) => isNumber(val) || isNil(val),
|
||||
}
|
||||
|
@ -68,6 +68,7 @@ import {
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { isNil } from 'lodash-unified'
|
||||
|
||||
import { ElIcon } from '@element-plus/components/icon'
|
||||
import { RepeatClick } from '@element-plus/directives'
|
||||
@ -79,14 +80,14 @@ import {
|
||||
useSize,
|
||||
} from '@element-plus/hooks'
|
||||
import ElInput from '@element-plus/components/input'
|
||||
import { debugWarn, isNumber, isUndefined } from '@element-plus/utils'
|
||||
import { debugWarn, isNumber, isString, isUndefined } from '@element-plus/utils'
|
||||
import { ArrowDown, ArrowUp, Minus, Plus } from '@element-plus/icons-vue'
|
||||
import { inputNumberEmits, inputNumberProps } from './input-number'
|
||||
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
|
||||
interface IData {
|
||||
currentValue: number | undefined
|
||||
currentValue: number | null | undefined
|
||||
userInput: null | number | string
|
||||
}
|
||||
|
||||
@ -116,10 +117,14 @@ export default defineComponent({
|
||||
const ns = useNamespace('input-number')
|
||||
|
||||
const minDisabled = computed(
|
||||
() => ensurePrecision(props.modelValue, -1) < props.min
|
||||
() =>
|
||||
isNumber(props.modelValue) &&
|
||||
ensurePrecision(props.modelValue, -1) < props.min
|
||||
)
|
||||
const maxDisabled = computed(
|
||||
() => ensurePrecision(props.modelValue) > props.max
|
||||
() =>
|
||||
isNumber(props.modelValue) &&
|
||||
ensurePrecision(props.modelValue) > props.max
|
||||
)
|
||||
|
||||
const numPrecision = computed(() => {
|
||||
@ -147,7 +152,8 @@ export default defineComponent({
|
||||
if (data.userInput !== null) {
|
||||
return data.userInput
|
||||
}
|
||||
let currentValue: number | string | undefined = data.currentValue
|
||||
let currentValue: number | string | undefined | null = data.currentValue
|
||||
if (isNil(currentValue)) return ''
|
||||
if (isNumber(currentValue)) {
|
||||
if (Number.isNaN(currentValue)) return ''
|
||||
if (!isUndefined(props.precision)) {
|
||||
@ -166,8 +172,8 @@ export default defineComponent({
|
||||
}
|
||||
return Number.parseFloat(`${Math.round(num * 10 ** pre) / 10 ** pre}`)
|
||||
}
|
||||
const getPrecision = (value: number | undefined) => {
|
||||
if (isUndefined(value)) return 0
|
||||
const getPrecision = (value: number | null | undefined) => {
|
||||
if (isNil(value)) return 0
|
||||
const valueString = value.toString()
|
||||
const dotPosition = valueString.indexOf('.')
|
||||
let precision = 0
|
||||
@ -179,7 +185,6 @@ export default defineComponent({
|
||||
const ensurePrecision = (val: number, coefficient: 1 | -1 = 1) => {
|
||||
if (!isNumber(val)) return data.currentValue
|
||||
// Solve the accuracy problem of JS decimal calculation by converting the value to integer.
|
||||
val = isNumber(val) ? val : Number.NaN
|
||||
return toPrecision(val + props.step * coefficient)
|
||||
}
|
||||
const increase = () => {
|
||||
@ -195,35 +200,38 @@ export default defineComponent({
|
||||
setCurrentValue(newVal)
|
||||
}
|
||||
const verifyValue = (
|
||||
value: number | string | undefined,
|
||||
value: number | string | null | undefined,
|
||||
update?: boolean
|
||||
): number | undefined => {
|
||||
const { max, min, step, precision, stepStrictly } = props
|
||||
): number | null | undefined => {
|
||||
const { max, min, step, precision, stepStrictly, valueOnClear } = props
|
||||
let newVal = Number(value)
|
||||
if (value === null) {
|
||||
newVal = Number.NaN
|
||||
if (isNil(value) || Number.isNaN(newVal)) {
|
||||
return null
|
||||
}
|
||||
if (!Number.isNaN(newVal)) {
|
||||
if (stepStrictly) {
|
||||
newVal = Math.round(newVal / step) * step
|
||||
}
|
||||
if (!isUndefined(precision)) {
|
||||
newVal = toPrecision(newVal, precision)
|
||||
}
|
||||
if (newVal > max || newVal < min) {
|
||||
newVal = newVal > max ? max : min
|
||||
update && emit('update:modelValue', newVal)
|
||||
if (value === '') {
|
||||
if (valueOnClear === null) {
|
||||
return null
|
||||
}
|
||||
newVal = isString(valueOnClear)
|
||||
? { min, max }[valueOnClear]
|
||||
: valueOnClear
|
||||
}
|
||||
if (stepStrictly) {
|
||||
newVal = Math.round(newVal / step) * step
|
||||
}
|
||||
if (!isUndefined(precision)) {
|
||||
newVal = toPrecision(newVal, precision)
|
||||
}
|
||||
if (newVal > max || newVal < min) {
|
||||
newVal = newVal > max ? max : min
|
||||
update && emit('update:modelValue', newVal)
|
||||
}
|
||||
return newVal
|
||||
}
|
||||
const setCurrentValue = (value: number | string | undefined) => {
|
||||
const setCurrentValue = (value: number | string | null | undefined) => {
|
||||
const oldVal = data.currentValue
|
||||
let newVal = verifyValue(value)
|
||||
const newVal = verifyValue(value)
|
||||
if (oldVal === newVal) return
|
||||
if (Number.isNaN(newVal)) {
|
||||
newVal = undefined
|
||||
}
|
||||
data.userInput = null
|
||||
emit('update:modelValue', newVal)
|
||||
emit('input', newVal)
|
||||
@ -262,22 +270,22 @@ export default defineComponent({
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
const newVal = verifyValue(value, true)
|
||||
data.currentValue = newVal
|
||||
data.currentValue = verifyValue(value, true)
|
||||
data.userInput = null
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
onMounted(() => {
|
||||
const { min, max, modelValue } = props
|
||||
const innerInput = input.value?.input as HTMLInputElement
|
||||
innerInput.setAttribute('role', 'spinbutton')
|
||||
if (Number.isFinite(props.max)) {
|
||||
innerInput.setAttribute('aria-valuemax', String(props.max))
|
||||
if (Number.isFinite(max)) {
|
||||
innerInput.setAttribute('aria-valuemax', String(max))
|
||||
} else {
|
||||
innerInput.removeAttribute('aria-valuemax')
|
||||
}
|
||||
if (Number.isFinite(props.min)) {
|
||||
innerInput.setAttribute('aria-valuemin', String(props.min))
|
||||
if (Number.isFinite(min)) {
|
||||
innerInput.setAttribute('aria-valuemin', String(min))
|
||||
} else {
|
||||
innerInput.removeAttribute('aria-valuemin')
|
||||
}
|
||||
@ -286,10 +294,10 @@ export default defineComponent({
|
||||
'aria-disabled',
|
||||
String(inputNumberDisabled.value)
|
||||
)
|
||||
if (!isNumber(props.modelValue)) {
|
||||
let val: number | undefined = Number(props.modelValue)
|
||||
if (!isNumber(modelValue) && modelValue != null) {
|
||||
let val: number | null = Number(modelValue)
|
||||
if (Number.isNaN(val)) {
|
||||
val = undefined
|
||||
val = null
|
||||
}
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user