0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ant Design の Input をカスタムして、数値入力用でフォーカスアウト時にカンマ区切りにする方法

Posted at

はじめに

React Typescript AntDesign を使用。
入力は数値のみ、フォーカスアウト時にカンマ区切りのフォーマットを適用する。 という実装をすることがあったのでメモを残す。
Ant Design には、InputNumber コンポーネントという数値入力専用のコンポーネントが存在するが、今回の要件にあるフォーカスアウト時にカンマ区切りにする。といったことができなかった。
デフォルトの Input でもこの動作をサポートしていないため、カスタムコンポーネント CustomInput を作成

Ant Design の Input とは?

Input は、フォームの入力フィールドとしてよく使われるコンポーネントで、以下のように簡単に導入できる。

import React, { useState } from 'react';
import { Input } from 'antd';

const BasicInput: React.FC = () => {
  const [value, setValue] = useState('');

  return <Input value={value} onChange={(e) => setValue(e.target.value)} placeholder="数値を入力" />;
};

export default BasicInput;

CustomInput の要件

数値のみを入力可能にする
最大桁数(maxLength)を設定可能にする
最小値・最大値の制限を設定可能にする
指定したステップで丸める
フォーカス時はカンマなし、フォーカスアウト時にカンマ区切りを適用
無効な値(数値以外)は自動クリア

実装コード

以下のコードが CustomInput の実装です。

import React, { forwardRef, useEffect, useState } from 'react'
import { Input, InputRef } from 'antd'
import { formatWithCommas } from '../../../utils/Format'

// 呼び出し元で渡せる値を定義
export interface CustomInputProps {
  value?: string // 初期値(文字列形式の数値)
  onChange?: (value: string) => void // 値変更時のコールバック関数
  placeholder?: string
  maxLength?: number // 最大桁数
  min?: number // 最小値
  max?: number // 最大値
  step?: number // ステップサイズ
  disabled?: boolean
}

const NumericInput = forwardRef<InputRef, NumericInputProps>((props, ref) => {
  const { value = '', onChange, placeholder, maxLength, min, max, step, onBlur, disabled = false } = props
  const [displayValue, setDisplayValue] = useState(value) // 表示用の値
  const [isFocused, setIsFocused] = useState(false) // フォーカス状態を管理

  // 初期値のフォーマットを適用
  // フォーカスの有無に応じて表示値を切り替える
  useEffect(() => {
    if (isFocused) {
      setDisplayValue(value)
    }
    else {
      setDisplayValue(formatWithCommas(value))
    }
  }, [value, isFocused])

  // 入力値をそのまま更新する(カンマなし)
  const updateInputValue = (value: string) => {
    if (!onChange) {
      return
    }

    onChange(value)
    setDisplayValue(value)
  }

  // カンマ区切りを適用して更新する
  const updateFormattedValue = (value: string | number) => {
    if (!onChange) {
      return null
    }
    const formattedValue = formatWithCommas(value) // カンマ区切り形式に変換
    onChange(String(value))
    setDisplayValue(formattedValue)
  }

  const constrainNumber = (numValue: number) => {
    // 入力値が min より小さい場合、最小値に丸める
    if (min !== undefined && numValue < min) {
      return min
    }

    // 入力値が max より大きい場合、最大値に丸める
    if (max !== undefined && max < numValue) {
      return max
    }

    // 入力値を step の倍数に丸める(浮動小数点誤差を回避)
    if (step) {
      const multiplier = 1 / step
      return Math.floor(numValue * multiplier) / multiplier
    }

    return numValue
  }

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (!onChange) {
      return
    }

    const { value: inputValue } = e.target
    const reg = /^-?\d*(\.\d*)?$/

    // 数値または有効な入力のみを許可
    if (reg.test(inputValue) || inputValue === '' || inputValue === '-') {
      // maxLength の指定がある場合、トリミング
      if (maxLength) {
        updateInputValue(inputValue.slice(0, maxLength))
      }
      else {
        updateInputValue(inputValue)
      }
    }
  }

  const handleBlur = () => {

    setIsFocused(false)
    
    if (!onChange || !value) {
      return
    }

    let numericValue = String(value)
    if (numericValue.charAt(numericValue.length - 1) === '.' || numericValue === '-') {
      numericValue = numericValue.slice(0, -1)
    }

    // 値を数値型に変換
    const numValue = parseFloat(numericValue)

    // 数値が有効かチェック
    if (isNaN(numValue)) {
      updateInputValue('')
      return
    }

    // カンマ区切りの表示値を設定
    updateFormattedValue(constrainNumber(numValue))
  }

  const handleFocus = () => {
    setIsFocused(true)
    // フォーカス時はカンマを除去した値を表示
    updateInputValue(value)
  }

  return (
    <Input
      value={displayValue} // 表示用の値を使用
      placeholder={placeholder}
      onChange={handleChange}
      onFocus={handleFocus}
      ref={ref}
      disabled={disabled}
    />
  )
})

export default CustomInput

正直、検証は細部までできていないので改善の余地は多々あると思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?