refactor(components): ElInputNumber (#3974)

* refactor(components): ElInputNumber

- Rename index.vue -> input-number.vue
- Extract inputNumberProps and inputNumberEmits to input-number.ts

* Update input number docs

* Update input number docs

* Update input number docs

* Address comments

* Code perfection

* Make modelValue required field

* Fix linter

* Remove unnecessary code

* Fix linter
This commit is contained in:
jeremywu
2021-10-22 23:28:03 +08:00
committed by GitHub
parent b2a0631a87
commit 6006e910aa
6 changed files with 127 additions and 120 deletions

View File

@@ -14,6 +14,12 @@ input-number/basic
:::
:::tip
When inputting invalid string to the input box, input value will emit `NaN` to the upper layer as result of error
:::
## Disabled
:::demo The `disabled` attribute accepts a `boolean`, and if the value is `true`, the component is disabled. If you just need to control the value within a range, you can add `min` attribute to set the minimum value and `max` to set the maximum value. By default, the minimum value is `0`.
@@ -92,11 +98,11 @@ input-number/controlled
## Events
| Event Name | Description | Parameters |
| ---------- | ------------------------------- | ---------------------- |
| change | triggers when the value changes | currentValue, oldValue |
| blur | triggers when Input blurs | (event: Event) |
| focus | triggers when Input focuses | (event: Event) |
| Event Name | Description | Parameters |
| ---------- | ------------------------------- | ------------------------------------------------------ |
| change | triggers when the value changes | (currentValue: number \| NaN, oldValue: number \| NaN) |
| blur | triggers when Input blurs | (event: Event) |
| focus | triggers when Input focuses | (event: Event) |
## Methods

View File

@@ -1,6 +1,6 @@
import { ref, nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import InputNumber from '../src/index.vue'
import InputNumber from '../src/input-number.vue'
const mouseup = new Event('mouseup')
const _mount = (options) =>
@@ -241,13 +241,13 @@ describe('InputNumber.vue', () => {
const elInput = wrapper.findComponent({ name: 'ElInputNumber' }).vm
elInput.handleInputChange('')
await nextTick()
expect(wrapper.vm.num).toBe(undefined)
expect(wrapper.vm.num).toBe(1)
elInput.increase()
await nextTick()
expect(wrapper.vm.num).toBe(1)
expect(wrapper.vm.num).toBe(2)
elInput.handleInputChange('')
await nextTick()
expect(wrapper.vm.num).toBe(undefined)
expect(wrapper.vm.num).toBe(1)
elInput.decrease()
await nextTick()
expect(wrapper.vm.num).toBe(1)

View File

@@ -1,13 +1,7 @@
import InputNumber from './src/index.vue'
import { withInstall } from '@element-plus/utils/with-install'
import InputNumber from './src/input-number.vue'
import type { App } from 'vue'
import type { SFCWithInstall } from '@element-plus/utils/types'
export const ElInputNumber = withInstall(InputNumber)
InputNumber.install = (app: App): void => {
app.component(InputNumber.name, InputNumber)
}
const _InputNumber = InputNumber as SFCWithInstall<typeof InputNumber>
export default _InputNumber
export const ElInputNumber = _InputNumber
export default ElInputNumber
export * from './src/input-number'

View File

@@ -0,0 +1,57 @@
import { buildProps, componentSize } from '@element-plus/utils/props'
import { isNumber } from '@element-plus/utils/util'
export const inputNumberProps = buildProps({
step: {
type: Number,
default: 1,
},
stepStrictly: {
type: Boolean,
default: false,
},
max: {
type: Number,
default: Infinity,
},
min: {
type: Number,
default: -Infinity,
},
modelValue: {
type: Number,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
size: {
type: String,
values: componentSize,
},
controls: {
type: Boolean,
default: true,
},
controlsPosition: {
type: String,
default: '',
values: ['', 'right'],
},
name: String,
label: String,
placeholder: String,
precision: {
type: Number,
validator: (val: number) => val >= 0 && val === parseInt(`${val}`, 10),
},
} as const)
export const inputNumberEmits = {
change: (prev: number, cur: number) => prev !== cur,
blur: (e: FocusEvent) => e instanceof FocusEvent,
focus: (e: FocusEvent) => e instanceof FocusEvent,
input: (val: number) => isNumber(val),
'update:modelValue': (val: number) => isNumber(val),
}

View File

@@ -55,24 +55,20 @@ import {
reactive,
ref,
watch,
inject,
onMounted,
onUpdated,
} from 'vue'
import { toRawType } from '@vue/shared'
import { RepeatClick } from '@element-plus/directives'
import { elFormKey, elFormItemKey } from '@element-plus/tokens'
import { useFormItem } from '@element-plus/hooks'
import ElInput from '@element-plus/components/input'
import { useGlobalConfig } from '@element-plus/utils/util'
import { isValidComponentSize } from '@element-plus/utils/validators'
import { isNumber } from '@element-plus/utils/util'
import { debugWarn } from '@element-plus/utils/error'
import { inputNumberProps, inputNumberEmits } from './input-number'
import type { PropType } from 'vue'
import type { ElFormContext, ElFormItemContext } from '@element-plus/tokens'
import type { ComponentSize } from '@element-plus/utils/types'
import type { ComponentPublicInstance } from 'vue'
interface IData {
currentValue: number | string
currentValue: number
userInput: null | number | string
}
@@ -84,68 +80,18 @@ export default defineComponent({
directives: {
RepeatClick,
},
props: {
step: {
type: Number,
default: 1,
},
stepStrictly: {
type: Boolean,
default: false,
},
max: {
type: Number,
default: Infinity,
},
min: {
type: Number,
default: -Infinity,
},
modelValue: {
type: Number,
},
disabled: {
type: Boolean,
default: false,
},
size: {
type: String as PropType<ComponentSize>,
validator: isValidComponentSize,
},
controls: {
type: Boolean,
default: true,
},
controlsPosition: {
type: String,
default: '',
},
name: String,
label: String,
placeholder: String,
precision: {
type: Number,
validator: (val: number) => val >= 0 && val === parseInt(`${val}`, 10),
},
},
emits: ['update:modelValue', 'change', 'input', 'blur', 'focus'],
props: inputNumberProps,
emits: inputNumberEmits,
setup(props, { emit }) {
const ELEMENT = useGlobalConfig()
const elForm = inject(elFormKey, {} as ElFormContext)
const elFormItem = inject(elFormItemKey, {} as ElFormItemContext)
const input = ref(null)
const input = ref<ComponentPublicInstance<typeof ElInput>>()
const data = reactive<IData>({
currentValue: props.modelValue,
userInput: null,
})
const minDisabled = computed(() => {
return _decrease(props.modelValue) < props.min
})
const maxDisabled = computed(() => {
return _increase(props.modelValue) > props.max
})
const minDisabled = computed(() => _decrease(props.modelValue) < props.min)
const maxDisabled = computed(() => _increase(props.modelValue) > props.max)
const numPrecision = computed(() => {
const stepPrecision = getPrecision(props.step)
if (props.precision !== undefined) {
@@ -163,31 +109,30 @@ export default defineComponent({
const controlsAtRight = computed(() => {
return props.controls && props.controlsPosition === 'right'
})
const inputNumberSize = computed(() => {
return props.size || elFormItem.size || ELEMENT.size
})
const inputNumberDisabled = computed(() => {
return props.disabled || elForm.disabled
})
const { size: inputNumberSize, disabled: inputNumberDisabled } =
useFormItem({})
const displayValue = computed(() => {
if (data.userInput !== null) {
return data.userInput
}
let currentValue = data.currentValue
if (typeof currentValue === 'number') {
let currentValue: number | string = data.currentValue
if (isNumber(currentValue)) {
if (Number.isNaN(currentValue)) return ''
if (props.precision !== undefined) {
currentValue = currentValue.toFixed(props.precision)
}
}
return currentValue
})
const toPrecision = (num, pre?) => {
const toPrecision = (num: number, pre?: number) => {
if (pre === undefined) pre = numPrecision.value
return parseFloat(
`${Math.round(num * Math.pow(10, pre)) / Math.pow(10, pre)}`
)
}
const getPrecision = (value) => {
const getPrecision = (value: number | undefined) => {
if (value === undefined) return 0
const valueString = value.toString()
const dotPosition = valueString.indexOf('.')
@@ -197,18 +142,20 @@ export default defineComponent({
}
return precision
}
const _increase = (val) => {
if (typeof val !== 'number' && val !== undefined) return data.currentValue
const _increase = (val: number) => {
if (!isNumber(val)) return data.currentValue
const precisionFactor = Math.pow(10, numPrecision.value)
// Solve the accuracy problem of JS decimal calculation by converting the value to integer.
val = isNumber(val) ? val : NaN
return toPrecision(
(precisionFactor * val + precisionFactor * props.step) / precisionFactor
)
}
const _decrease = (val) => {
if (typeof val !== 'number' && val !== undefined) return data.currentValue
const _decrease = (val: number) => {
if (!isNumber(val)) return data.currentValue
const precisionFactor = Math.pow(10, numPrecision.value)
// Solve the accuracy problem of JS decimal calculation by converting the value to integer.
val = isNumber(val) ? val : NaN
return toPrecision(
(precisionFactor * val - precisionFactor * props.step) / precisionFactor
)
@@ -225,7 +172,7 @@ export default defineComponent({
const newVal = _decrease(value)
setCurrentValue(newVal)
}
const setCurrentValue = (newVal) => {
const setCurrentValue = (newVal: number) => {
const oldVal = data.currentValue
if (typeof newVal === 'number' && props.precision !== undefined) {
newVal = toPrecision(newVal, props.precision)
@@ -233,35 +180,38 @@ export default defineComponent({
if (newVal !== undefined && newVal >= props.max) newVal = props.max
if (newVal !== undefined && newVal <= props.min) newVal = props.min
if (oldVal === newVal) return
if (!isNumber(newVal)) {
newVal = NaN
}
data.userInput = null
emit('update:modelValue', newVal)
emit('input', newVal)
emit('change', newVal, oldVal)
data.currentValue = newVal
}
const handleInput = (value) => {
const handleInput = (value: string) => {
return (data.userInput = value)
}
const handleInputChange = (value) => {
const newVal = value === '' ? undefined : Number(value)
if (!isNaN(newVal) || value === '') {
const handleInputChange = (value: string) => {
const newVal = Number(value)
if ((isNumber(newVal) && !Number.isNaN(newVal)) || value === '') {
setCurrentValue(newVal)
}
data.userInput = null
}
const focus = () => {
input.value.focus?.()
input.value?.focus?.()
}
const blur = () => {
input.value.blur?.()
input.value?.blur?.()
}
watch(
() => props.modelValue,
(value) => {
let newVal = value === undefined ? value : Number(value)
let newVal = Number(value)
if (newVal !== undefined) {
if (isNaN(newVal)) return
if (props.stepStrictly) {
@@ -289,21 +239,21 @@ export default defineComponent({
{ immediate: true }
)
onMounted(() => {
const innerInput = input.value.input
const innerInput = input.value?.input as HTMLInputElement
innerInput.setAttribute('role', 'spinbutton')
innerInput.setAttribute('aria-valuemax', props.max)
innerInput.setAttribute('aria-valuemin', props.min)
innerInput.setAttribute('aria-valuenow', data.currentValue)
innerInput.setAttribute('aria-disabled', inputNumberDisabled.value)
if (
toRawType(props.modelValue) !== 'Number' &&
props.modelValue !== undefined
) {
emit('update:modelValue', undefined)
innerInput.setAttribute('aria-valuemax', String(props.max))
innerInput.setAttribute('aria-valuemin', String(props.min))
innerInput.setAttribute('aria-valuenow', String(data.currentValue))
innerInput.setAttribute(
'aria-disabled',
String(inputNumberDisabled.value)
)
if (!isNumber(props.modelValue)) {
emit('update:modelValue', Number(props.modelValue))
}
})
onUpdated(() => {
const innerInput = input.value.input
const innerInput = input.value?.input
innerInput.setAttribute('aria-valuenow', data.currentValue)
})
return {

View File

@@ -25,11 +25,11 @@ export type LocalFallbacks = {
}
export const useFormItem = ({ size, disabled }: LocalFallbacks) => {
const vm = getCurrentInstance()
const vm = getCurrentInstance()!
const $ELEMENT = useGlobalConfig()
// vm.props is not reactive so we use the reactive one here.
const props = vm.proxy.$props as UseFormItemProps
const props = vm.proxy?.$props as UseFormItemProps
const form = inject(elFormKey, undefined)
const formItem = inject(elFormItemKey, undefined)