Search code examples
javascriptformattingnumbers

`toLocaleString()` not rendering inside number input when value greater than 1 million


In this code, I have the value 1000000 (one million) which I format with toLocaleString('en-gb') to get 1,000,000.

Then I print that value as text and it works as expected, also when I use that const as the value of a text input. But when using the value in a numeric input, it just doesn't render. It works though when the value is < 1 million.

enter image description here

Inspecting the html, I see the value is correct: enter image description here

In addition, when trying to type the value in that numeric input, it doesn't register values after 4 digits.

Any ideas what's going on? I wonder if it could be that after 999.000 the number will have two thousand-separators

teh codez (also in this playground https://stackblitz.com/edit/react-ts-8ufbe1?file=App.tsx):

export default function App() {
  const value = (1000000).toLocaleString('en-gb');
  const [inputValue, setInputValue] = React.useState(value);

  return (
    <div>
      <h1>{value}</h1>
      <input
        type="number"
        value={inputValue}
        onChange={(e) => setInputValue(Number(e.target.value).toLocaleString('en-gb'))}
      />
      <input type="text" value={inputValue} />
    </div>
  );
}

I see there are libraries like react-number-format but seems like the native way should do what I need.

Thank you very much in advance.


Solution

  • It works before reaching 1,000 because 1,000 is where you have to add a comma. At 999 you still satisfy the requirements of an input with type number you're inserting only a number 999. Since HTML only really allows strings they let you pass with "999" vs 999 the pure numeric version.

    In any case. I don't think you want to use the type='number' it works pretty poorly and shows the ticker up and down. If this is a mobile focused decision consider using type='tel'.

    What you can also do is attempt to manually mask the behavior by putting 2 inputs on top of each other. Or you can use an input type='tel' onFocus={() => Number(inputValue)} and onBlur={() => inputValue.toLocaleString('en-gb')} and that will be the start to getting where you want to get.

    This type of stuff usually is slightly messy and will take a bit to perfect. The UI can easily get wonky, and the problem is not so perfectly solved:

    • We want to enforce that users only type numbers and make it easy for them to do so (especially mobile)
    • We want the numbers to appear as strings. Those 2 priorities are at odds with each other.

    If you don't want to directly import a library which contains this I'd strongly suggest you read their source code.

    Some potential resources: https://cchanxzy.github.io/react-currency-input-field/ (Note that just the single component is 600 lines long)

    including this excerpt:

    const handleOnFocus = (event: React.FocusEvent<HTMLInputElement>): number => {
          onFocus && onFocus(event);
          return stateValue ? stateValue.length : 0;
        };
    
        /**
         * Handle blur event
         *
         * Format value by padding/trimming decimals if required by
         */
        const handleOnBlur = (event: React.FocusEvent<HTMLInputElement>): void => {
          const {
            target: { value },
          } = event;
    
          const valueOnly = cleanValue({ value, ...cleanValueOptions });
    
          if (valueOnly === '-' || !valueOnly) {
            setStateValue('');
            onBlur && onBlur(event);
            return;
          }
    
          const fixedDecimals = fixedDecimalValue(valueOnly, decimalSeparator, fixedDecimalLength);
    
          const newValue = padTrimValue(
            fixedDecimals,
            decimalSeparator,
            decimalScale !== undefined ? decimalScale : fixedDecimalLength
          );
    
          const numberValue = parseFloat(newValue.replace(decimalSeparator, '.'));
    
          const formattedValue = formatValue({
            ...formatValueOptions,
            value: newValue,
          });
    
          if (onValueChange) {
            onValueChange(newValue, name, {
              float: numberValue,
              formatted: formattedValue,
              value: newValue,
            });
          }
    
          setStateValue(formattedValue);
    
          onBlur && onBlur(event);
        };
        ```