InputNumber组件扩展
刚开始用React,有不对的地方,欢迎指正
最近在做Json编辑器时,使用到InputNumber,但是ant-design的InputNumber不是在Input组件内部实现的,很多Input的扩展功能(如addonAffter ,suffix,prefix等)都没有。然后编辑器的表单字段需要支持addonAfter这些字段,需要在输入组件的粒度上实现这些扩展功能。
首先,查下有没有现成的解决方案。搜索了下ant-design的Issues,可以找到相关的提议#14284,官方建议是使用Input的type=number功能。但是我又需要InputNumber的自动转换类型和精度等的控制,所以只能自己实现了。
解决方案
在现有基础上改造有以下解决方案:
- 基于InputNumber,实现Input的相关功能
- 基于Input组件,实现InputNumber的相关功能
由于Input中的addons,prefix,suffix这些实现都需要改动DOM结构,实现起来很繁琐。所以采取基于Input,实现InputNumber功能。
查看了下ant-design的代码,InputNumber的主要逻辑实现在rc-input-number中。是使用React组件的形式实现的。大致看了下源码,接下来需要注意的是:
- 输入字符串转数字,可能会被转成科学计数方式,借鉴官方方式,使用toFixed方法转化
- 如果用户在输入中的时候(Input在Focus状态),不进行parse
功能
首先,整理下我需要实现的InputNumber的功能吧
- min/max/precision 数字大小和精度的控制
- 用户输入完成如果不符合要求的话,要求自动转化或回滚到上一次的值
- step 步数控制,可为小数
- 表单通用设置:defaultValue、value、onChange
原InputNumber的键盘操作什么的,暂时不加了
实现
最近用惯了Hook,还是继续使用函数实现吧。
主要是使用Input组件,指定type为number,min和max属性还是设置在Input上,继续使用原生的number输入。
直接贴代码:
import React, { useCallback, useState, useEffect, RefObject } from 'react'
import { Input } from 'antd';
import { InputProps } from 'antd/lib/input';
interface IInputNumberExtend extends Omit<InputProps, 'onChange' | 'value'> {
max?: number;
min?: number;
precision?: number;
step?: number;
autoFocus?: boolean;
value?: number;
onChange?: (value: number | undefined) => void;
}
/**
* InputString convert to number
* @param strNum
* @param option
*/
function inputStrToNum(strNum: string | undefined, option: { precision?: number, min?: number, max?: number }) {
if (!strNum) {
return;
}
let num = parseFloat(strNum);
return parseNum(num, option);
}
function parseNum(num: number, option: { precision?: number, min?: number, max?: number }) {
if (isNaN(num)) {
return;
}
// parse By precision
if (option.precision && option.precision > 0) {
let precisionPow = Math.pow(10, option.precision);
num = Math.round(num * precisionPow) / precisionPow;
}
if (option.min != null) {
if (num < option.min) {
num = option.min;
return num;
}
}
if (option.max != null) {
if (num > option.max) {
num = option.max;
return num;
}
}
return num;
}
/**
* number convert to display string
* @param num
* @param precision
*/
function numToStr(num: number, precision?: number) {
if (typeof num === 'string') {
return num;
}
if (isNaN(num)) {
return num + '';
}
if (precision != null) {
return num.toFixed(precision);
} else {
if (!/e/i.test(String(num))) {
return num + '';
}
let str = Number(num).toFixed(18).replace(/\.?0+$/, '');
return str;
}
}
function InputNumberExtend(props: IInputNumberExtend, ref: React.MutableRefObject<Input | null> | ((instance: Input | null) => void) | null): React.ReactElement {
const {
max,
min,
precision,
step,
value,
onChange,
onBlur,
onFocus,
autoFocus,
...restOption
} = props;
const [focused, setFocused] = useState(autoFocus);
const [inputValue, setInputValue] = useState<string | undefined>(undefined);
useEffect(() => {
if (value == null) {
setInputValue(undefined);
return;
}
if (typeof value === 'number') {
let num = parseNum(value, { precision, min, max });
if (num != null) {
setInputValue(numToStr(num, precision))
} else {
onChange && onChange(undefined);
}
} else {
let iNum = inputStrToNum(value, { precision, min, max });
if (iNum == null) {
onChange && onChange(undefined);
} else {
onChange && onChange(iNum);
}
}
}, [max, min, onChange, precision, value])
const onFocusHandle = useCallback((e) => {
setFocused(true);
onFocus && onFocus(e);
}, [onFocus]);
const onBlurHandle = useCallback((e) => {
setFocused(false);
onBlur && onBlur(e);
let parsedNum = inputStrToNum(inputValue, { precision, min, max });
if (parsedNum !== value) {
if (parsedNum) {
setInputValue(numToStr(parsedNum, precision));
}
onChange && onChange(parsedNum);
}
}, [inputValue, max, min, onBlur, onChange, precision, value]);
const inputOnChange = useCallback(
(e) => {
let strV = e.target.value.trim();
if (focused) {
setInputValue(strV);
} else {
// 重置value
let parsedNum = inputStrToNum(strV, { precision, min, max });
if (parsedNum != null) {
onChange && onChange(parsedNum);
}
// else {
// // Input invalid,do nothing
// }
}
},
[focused, max, min, onChange, precision],
)
return <Input
{...restOption}
ref={ref}
type="number"
autoFocus={autoFocus}
min={min}
max={max}
step={step}
onChange={inputOnChange}
value={inputValue}
onFocus={onFocusHandle}
onBlur={onBlurHandle}
/>
}
// export default React.memo(InputNumberExtend); 忘记考虑ref了
export default React.memo(React.forwardRef<Input, IInputNumberExtend>(InputNumberExtend));