记一次InputNumber组件扩展

4,625 阅读3分钟

InputNumber组件扩展

刚开始用React,有不对的地方,欢迎指正

最近在做Json编辑器时,使用到InputNumber,但是ant-design的InputNumber不是在Input组件内部实现的,很多Input的扩展功能(如addonAffter ,suffix,prefix等)都没有。然后编辑器的表单字段需要支持addonAfter这些字段,需要在输入组件的粒度上实现这些扩展功能。

首先,查下有没有现成的解决方案。搜索了下ant-design的Issues,可以找到相关的提议#14284,官方建议是使用Input的type=number功能。但是我又需要InputNumber的自动转换类型和精度等的控制,所以只能自己实现了。

解决方案

在现有基础上改造有以下解决方案:

  1. 基于InputNumber,实现Input的相关功能
  2. 基于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输入。

Edit antd-ts-inputNumberExtend-palyyard

直接贴代码:

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));