mirror of
https://github.com/element-plus/element-plus.git
synced 2025-03-07 15:47:57 +08:00
refactor(components): [input-number] switch to script-setup syntax (#7888)
This commit is contained in:
parent
ca658915dd
commit
ffd83fda93
@ -4,7 +4,6 @@ import { describe, expect, it, test } from 'vitest'
|
||||
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue'
|
||||
import { ElFormItem } from '@element-plus/components/form'
|
||||
import InputNumber from '../src/input-number.vue'
|
||||
import type { InputNumberInstance } from '../src/input-number'
|
||||
|
||||
const mouseup = new Event('mouseup')
|
||||
|
||||
@ -305,36 +304,50 @@ describe('InputNumber.vue', () => {
|
||||
test('check increase and decrease button when modelValue not in [min, max]', async () => {
|
||||
const num1 = ref(-5)
|
||||
const num2 = ref(15)
|
||||
const inputNumber1 = ref<InputNumberInstance>()
|
||||
const inputNumber2 = ref<InputNumberInstance>()
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => (
|
||||
<>
|
||||
<InputNumber
|
||||
ref="inputNumber1"
|
||||
v-model={num1.value}
|
||||
min={1}
|
||||
max={10}
|
||||
/>
|
||||
<InputNumber
|
||||
ref="inputNumber2"
|
||||
v-model={num2.value}
|
||||
min={1}
|
||||
max={10}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
mount(() => (
|
||||
<>
|
||||
<InputNumber ref={inputNumber1} v-model={num1.value} min={1} max={10} />
|
||||
<InputNumber ref={inputNumber2} v-model={num2.value} min={1} max={10} />
|
||||
</>
|
||||
))
|
||||
const inputNumber1 = wrapper.findComponent({ ref: 'inputNumber1' }).vm
|
||||
const inputNumber2 = wrapper.findComponent({ ref: 'inputNumber2' }).vm
|
||||
|
||||
expect(num1.value).toBe(1)
|
||||
expect(num2.value).toBe(10)
|
||||
|
||||
inputNumber1.value!.decrease()
|
||||
inputNumber1.decrease()
|
||||
await nextTick()
|
||||
expect(num1.value).toBe(1)
|
||||
inputNumber1.value!.increase()
|
||||
inputNumber1.increase()
|
||||
await nextTick()
|
||||
expect(num1.value).toBe(2)
|
||||
inputNumber1.value!.increase()
|
||||
inputNumber1.increase()
|
||||
await nextTick()
|
||||
expect(num1.value).toBe(3)
|
||||
|
||||
inputNumber2.value!.increase()
|
||||
inputNumber2.increase()
|
||||
await nextTick()
|
||||
expect(num2.value).toBe(10)
|
||||
inputNumber2.value!.decrease()
|
||||
inputNumber2.decrease()
|
||||
await nextTick()
|
||||
expect(num2.value).toBe(9)
|
||||
inputNumber2.value!.decrease()
|
||||
inputNumber2.decrease()
|
||||
await nextTick()
|
||||
expect(num2.value).toBe(8)
|
||||
})
|
||||
|
@ -58,20 +58,12 @@
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
onMounted,
|
||||
onUpdated,
|
||||
reactive,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue'
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUpdated, reactive, ref, watch } from 'vue'
|
||||
import { isNil } from 'lodash-unified'
|
||||
|
||||
import { ElInput } from '@element-plus/components/input'
|
||||
import { ElIcon } from '@element-plus/components/icon'
|
||||
import { RepeatClick } from '@element-plus/directives'
|
||||
import { RepeatClick as vRepeatClick } from '@element-plus/directives'
|
||||
import {
|
||||
useDisabled,
|
||||
useFormItem,
|
||||
@ -79,257 +71,226 @@ import {
|
||||
useNamespace,
|
||||
useSize,
|
||||
} from '@element-plus/hooks'
|
||||
import ElInput from '@element-plus/components/input'
|
||||
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 { InputInstance } from '@element-plus/components/input'
|
||||
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
defineOptions({
|
||||
name: 'ElInputNumber',
|
||||
})
|
||||
|
||||
interface IData {
|
||||
const props = defineProps(inputNumberProps)
|
||||
const emit = defineEmits(inputNumberEmits)
|
||||
|
||||
const { t } = useLocale()
|
||||
const ns = useNamespace('input-number')
|
||||
const input = ref<InputInstance>()
|
||||
|
||||
interface Data {
|
||||
currentValue: number | null | undefined
|
||||
userInput: null | number | string
|
||||
}
|
||||
const data = reactive<Data>({
|
||||
currentValue: props.modelValue,
|
||||
userInput: null,
|
||||
})
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElInputNumber',
|
||||
components: {
|
||||
ElInput,
|
||||
ElIcon,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
Plus,
|
||||
Minus,
|
||||
},
|
||||
directives: {
|
||||
RepeatClick,
|
||||
},
|
||||
props: inputNumberProps,
|
||||
emits: inputNumberEmits,
|
||||
setup(props, { emit }) {
|
||||
const input = ref<ComponentPublicInstance<typeof ElInput>>()
|
||||
const data = reactive<IData>({
|
||||
currentValue: props.modelValue,
|
||||
userInput: null,
|
||||
})
|
||||
const { t } = useLocale()
|
||||
const { formItem } = useFormItem()
|
||||
const ns = useNamespace('input-number')
|
||||
const { formItem } = useFormItem()
|
||||
|
||||
const minDisabled = computed(
|
||||
() =>
|
||||
isNumber(props.modelValue) &&
|
||||
ensurePrecision(props.modelValue, -1) < props.min
|
||||
)
|
||||
const maxDisabled = computed(
|
||||
() =>
|
||||
isNumber(props.modelValue) &&
|
||||
ensurePrecision(props.modelValue) > props.max
|
||||
)
|
||||
const minDisabled = computed(
|
||||
() =>
|
||||
isNumber(props.modelValue) &&
|
||||
ensurePrecision(props.modelValue, -1)! < props.min
|
||||
)
|
||||
const maxDisabled = computed(
|
||||
() =>
|
||||
isNumber(props.modelValue) && ensurePrecision(props.modelValue)! > props.max
|
||||
)
|
||||
|
||||
const numPrecision = computed(() => {
|
||||
const stepPrecision = getPrecision(props.step)
|
||||
if (!isUndefined(props.precision)) {
|
||||
if (stepPrecision > props.precision) {
|
||||
debugWarn(
|
||||
'InputNumber',
|
||||
'precision should not be less than the decimal places of step'
|
||||
)
|
||||
}
|
||||
return props.precision
|
||||
} else {
|
||||
return Math.max(getPrecision(props.modelValue), stepPrecision)
|
||||
}
|
||||
})
|
||||
const controlsAtRight = computed(() => {
|
||||
return props.controls && props.controlsPosition === 'right'
|
||||
})
|
||||
|
||||
const inputNumberSize = useSize()
|
||||
const inputNumberDisabled = useDisabled()
|
||||
|
||||
const displayValue = computed(() => {
|
||||
if (data.userInput !== null) {
|
||||
return data.userInput
|
||||
}
|
||||
let currentValue: number | string | undefined | null = data.currentValue
|
||||
if (isNil(currentValue)) return ''
|
||||
if (isNumber(currentValue)) {
|
||||
if (Number.isNaN(currentValue)) return ''
|
||||
if (!isUndefined(props.precision)) {
|
||||
currentValue = currentValue.toFixed(props.precision)
|
||||
}
|
||||
}
|
||||
return currentValue
|
||||
})
|
||||
const toPrecision = (num: number, pre?: number) => {
|
||||
if (isUndefined(pre)) pre = numPrecision.value
|
||||
if (pre === 0) return Math.round(num)
|
||||
let snum = String(num)
|
||||
const pointPos = snum.indexOf('.')
|
||||
if (pointPos === -1) return num
|
||||
const nums = snum.replace('.', '').split('')
|
||||
const datum = nums[pointPos + pre]
|
||||
if (!datum) return num
|
||||
const length = snum.length
|
||||
if (snum.charAt(length - 1) === '5') {
|
||||
snum = `${snum.slice(0, Math.max(0, length - 1))}6`
|
||||
}
|
||||
return Number.parseFloat(Number(snum).toFixed(pre))
|
||||
}
|
||||
const getPrecision = (value: number | null | undefined) => {
|
||||
if (isNil(value)) return 0
|
||||
const valueString = value.toString()
|
||||
const dotPosition = valueString.indexOf('.')
|
||||
let precision = 0
|
||||
if (dotPosition !== -1) {
|
||||
precision = valueString.length - dotPosition - 1
|
||||
}
|
||||
return precision
|
||||
}
|
||||
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.
|
||||
return toPrecision(val + props.step * coefficient)
|
||||
}
|
||||
const increase = () => {
|
||||
if (inputNumberDisabled.value || maxDisabled.value) return
|
||||
const value = props.modelValue || 0
|
||||
const newVal = ensurePrecision(value)
|
||||
setCurrentValue(newVal)
|
||||
}
|
||||
const decrease = () => {
|
||||
if (inputNumberDisabled.value || minDisabled.value) return
|
||||
const value = props.modelValue || 0
|
||||
const newVal = ensurePrecision(value, -1)
|
||||
setCurrentValue(newVal)
|
||||
}
|
||||
const verifyValue = (
|
||||
value: number | string | null | undefined,
|
||||
update?: boolean
|
||||
): number | null | undefined => {
|
||||
const { max, min, step, precision, stepStrictly, valueOnClear } = props
|
||||
let newVal = Number(value)
|
||||
if (isNil(value) || Number.isNaN(newVal)) {
|
||||
return null
|
||||
}
|
||||
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 | null | undefined) => {
|
||||
const oldVal = data.currentValue
|
||||
const newVal = verifyValue(value)
|
||||
if (oldVal === newVal) return
|
||||
data.userInput = null
|
||||
emit('update:modelValue', newVal)
|
||||
emit('input', newVal)
|
||||
emit('change', newVal, oldVal)
|
||||
formItem?.validate?.('change').catch((err) => debugWarn(err))
|
||||
data.currentValue = newVal
|
||||
}
|
||||
const handleInput = (value: string) => {
|
||||
return (data.userInput = value)
|
||||
}
|
||||
const handleInputChange = (value: string) => {
|
||||
const newVal = value !== '' ? Number(value) : ''
|
||||
if ((isNumber(newVal) && !Number.isNaN(newVal)) || value === '') {
|
||||
setCurrentValue(newVal)
|
||||
}
|
||||
data.userInput = null
|
||||
}
|
||||
|
||||
const focus = () => {
|
||||
input.value?.focus?.()
|
||||
}
|
||||
|
||||
const blur = () => {
|
||||
input.value?.blur?.()
|
||||
}
|
||||
|
||||
const handleFocus = (event: MouseEvent | FocusEvent) => {
|
||||
emit('focus', event)
|
||||
}
|
||||
|
||||
const handleBlur = (event: MouseEvent | FocusEvent) => {
|
||||
emit('blur', event)
|
||||
formItem?.validate?.('blur').catch((err) => debugWarn(err))
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
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(max)) {
|
||||
innerInput.setAttribute('aria-valuemax', String(max))
|
||||
} else {
|
||||
innerInput.removeAttribute('aria-valuemax')
|
||||
}
|
||||
if (Number.isFinite(min)) {
|
||||
innerInput.setAttribute('aria-valuemin', String(min))
|
||||
} else {
|
||||
innerInput.removeAttribute('aria-valuemin')
|
||||
}
|
||||
innerInput.setAttribute('aria-valuenow', String(data.currentValue))
|
||||
innerInput.setAttribute(
|
||||
'aria-disabled',
|
||||
String(inputNumberDisabled.value)
|
||||
const numPrecision = computed(() => {
|
||||
const stepPrecision = getPrecision(props.step)
|
||||
if (!isUndefined(props.precision)) {
|
||||
if (stepPrecision > props.precision) {
|
||||
debugWarn(
|
||||
'InputNumber',
|
||||
'precision should not be less than the decimal places of step'
|
||||
)
|
||||
if (!isNumber(modelValue) && modelValue != null) {
|
||||
let val: number | null = Number(modelValue)
|
||||
if (Number.isNaN(val)) {
|
||||
val = null
|
||||
}
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
})
|
||||
onUpdated(() => {
|
||||
const innerInput = input.value?.input
|
||||
innerInput?.setAttribute('aria-valuenow', data.currentValue)
|
||||
})
|
||||
return {
|
||||
t,
|
||||
input,
|
||||
displayValue,
|
||||
handleInput,
|
||||
handleInputChange,
|
||||
controlsAtRight,
|
||||
decrease,
|
||||
increase,
|
||||
inputNumberSize,
|
||||
inputNumberDisabled,
|
||||
maxDisabled,
|
||||
minDisabled,
|
||||
focus,
|
||||
blur,
|
||||
handleFocus,
|
||||
handleBlur,
|
||||
ns,
|
||||
}
|
||||
return props.precision
|
||||
} else {
|
||||
return Math.max(getPrecision(props.modelValue), stepPrecision)
|
||||
}
|
||||
})
|
||||
const controlsAtRight = computed(() => {
|
||||
return props.controls && props.controlsPosition === 'right'
|
||||
})
|
||||
|
||||
const inputNumberSize = useSize()
|
||||
const inputNumberDisabled = useDisabled()
|
||||
|
||||
const displayValue = computed(() => {
|
||||
if (data.userInput !== null) {
|
||||
return data.userInput
|
||||
}
|
||||
let currentValue: number | string | undefined | null = data.currentValue
|
||||
if (isNil(currentValue)) return ''
|
||||
if (isNumber(currentValue)) {
|
||||
if (Number.isNaN(currentValue)) return ''
|
||||
if (!isUndefined(props.precision)) {
|
||||
currentValue = currentValue.toFixed(props.precision)
|
||||
}
|
||||
}
|
||||
return currentValue
|
||||
})
|
||||
const toPrecision = (num: number, pre?: number) => {
|
||||
if (isUndefined(pre)) pre = numPrecision.value
|
||||
if (pre === 0) return Math.round(num)
|
||||
let snum = String(num)
|
||||
const pointPos = snum.indexOf('.')
|
||||
if (pointPos === -1) return num
|
||||
const nums = snum.replace('.', '').split('')
|
||||
const datum = nums[pointPos + pre]
|
||||
if (!datum) return num
|
||||
const length = snum.length
|
||||
if (snum.charAt(length - 1) === '5') {
|
||||
snum = `${snum.slice(0, Math.max(0, length - 1))}6`
|
||||
}
|
||||
return Number.parseFloat(Number(snum).toFixed(pre))
|
||||
}
|
||||
const getPrecision = (value: number | null | undefined) => {
|
||||
if (isNil(value)) return 0
|
||||
const valueString = value.toString()
|
||||
const dotPosition = valueString.indexOf('.')
|
||||
let precision = 0
|
||||
if (dotPosition !== -1) {
|
||||
precision = valueString.length - dotPosition - 1
|
||||
}
|
||||
return precision
|
||||
}
|
||||
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.
|
||||
return toPrecision(val + props.step * coefficient)
|
||||
}
|
||||
const increase = () => {
|
||||
if (inputNumberDisabled.value || maxDisabled.value) return
|
||||
const value = props.modelValue || 0
|
||||
const newVal = ensurePrecision(value)
|
||||
setCurrentValue(newVal)
|
||||
}
|
||||
const decrease = () => {
|
||||
if (inputNumberDisabled.value || minDisabled.value) return
|
||||
const value = props.modelValue || 0
|
||||
const newVal = ensurePrecision(value, -1)
|
||||
setCurrentValue(newVal)
|
||||
}
|
||||
const verifyValue = (
|
||||
value: number | string | null | undefined,
|
||||
update?: boolean
|
||||
): number | null | undefined => {
|
||||
const { max, min, step, precision, stepStrictly, valueOnClear } = props
|
||||
let newVal = Number(value)
|
||||
if (isNil(value) || Number.isNaN(newVal)) {
|
||||
return null
|
||||
}
|
||||
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 | null | undefined) => {
|
||||
const oldVal = data.currentValue
|
||||
const newVal = verifyValue(value)
|
||||
if (oldVal === newVal) return
|
||||
data.userInput = null
|
||||
emit('update:modelValue', newVal!)
|
||||
emit('input', newVal)
|
||||
emit('change', newVal!, oldVal!)
|
||||
formItem?.validate?.('change').catch((err) => debugWarn(err))
|
||||
data.currentValue = newVal
|
||||
}
|
||||
const handleInput = (value: string) => {
|
||||
return (data.userInput = value)
|
||||
}
|
||||
const handleInputChange = (value: string) => {
|
||||
const newVal = value !== '' ? Number(value) : ''
|
||||
if ((isNumber(newVal) && !Number.isNaN(newVal)) || value === '') {
|
||||
setCurrentValue(newVal)
|
||||
}
|
||||
data.userInput = null
|
||||
}
|
||||
|
||||
const focus = () => {
|
||||
input.value?.focus?.()
|
||||
}
|
||||
|
||||
const blur = () => {
|
||||
input.value?.blur?.()
|
||||
}
|
||||
|
||||
const handleFocus = (event: MouseEvent | FocusEvent) => {
|
||||
emit('focus', event)
|
||||
}
|
||||
|
||||
const handleBlur = (event: MouseEvent | FocusEvent) => {
|
||||
emit('blur', event)
|
||||
formItem?.validate?.('blur').catch((err) => debugWarn(err))
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
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(max)) {
|
||||
innerInput.setAttribute('aria-valuemax', String(max))
|
||||
} else {
|
||||
innerInput.removeAttribute('aria-valuemax')
|
||||
}
|
||||
if (Number.isFinite(min)) {
|
||||
innerInput.setAttribute('aria-valuemin', String(min))
|
||||
} else {
|
||||
innerInput.removeAttribute('aria-valuemin')
|
||||
}
|
||||
innerInput.setAttribute('aria-valuenow', String(data.currentValue))
|
||||
innerInput.setAttribute('aria-disabled', String(inputNumberDisabled.value))
|
||||
if (!isNumber(modelValue) && modelValue != null) {
|
||||
let val: number | null = Number(modelValue)
|
||||
if (Number.isNaN(val)) {
|
||||
val = null
|
||||
}
|
||||
emit('update:modelValue', val!)
|
||||
}
|
||||
})
|
||||
onUpdated(() => {
|
||||
const innerInput = input.value?.input
|
||||
innerInput?.setAttribute('aria-valuenow', `${data.currentValue}`)
|
||||
})
|
||||
defineExpose({
|
||||
/** @description get focus the input component */
|
||||
focus,
|
||||
/** @description remove focus the input component */
|
||||
blur,
|
||||
})
|
||||
</script>
|
||||
|
Loading…
Reference in New Issue
Block a user