記一次InputNumber組件擴展

InputNumber組件擴展

剛開始用React,有不對的地方,歡迎指正react

最近在作Json編輯器時,使用到InputNumber,可是ant-design的InputNumber不是在Input組件內部實現的,不少Input的擴展功能(如addonAffter ,suffix,prefix等)都沒有。而後編輯器的表單字段須要支持addonAfter這些字段,須要在輸入組件的粒度上實現這些擴展功能。git

首先,查下有沒有現成的解決方案。搜索了下ant-design的Issues,能夠找到相關的提議#14284,官方建議是使用Input的type=number功能。可是我又須要InputNumber的自動轉換類型和精度等的控制,因此只能本身實現了。github

解決方案

在現有基礎上改造有如下解決方案:antd

  1. 基於InputNumber,實現Input的相關功能
  2. 基於Input組件,實現InputNumber的相關功能

因爲Input中的addons,prefix,suffix這些實現都須要改動DOM結構,實現起來很繁瑣。因此採起基於Input,實現InputNumber功能。編輯器

查看了下ant-design的代碼,InputNumber的主要邏輯實如今rc-input-number中。是使用React組件的形式實現的。大體看了下源碼,接下來須要注意的是:ide

  • 輸入字符串轉數字,可能會被轉成科學計數方式,借鑑官方方式,使用toFixed方法轉化
  • 若是用戶在輸入中的時候(Input在Focus狀態),不進行parse

功能

首先,整理下我須要實現的InputNumber的功能吧函數

  • min/max/precision 數字大小和精度的控制
    • 用戶輸入完成若是不符合要求的話,要求自動轉化或回滾到上一次的值
  • step 步數控制,可爲小數
  • 表單通用設置:defaultValue、value、onChange

原InputNumber的鍵盤操做什麼的,暫時不加了ui

實現

最近用慣了Hook,仍是繼續使用函數實現吧。spa

主要是使用Input組件,指定type爲number,min和max屬性仍是設置在Input上,繼續使用原生的number輸入。rest

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


複製代碼
相關文章
相關標籤/搜索