react-number-format icon indicating copy to clipboard operation
react-number-format copied to clipboard

How to use currency format to fill decimal/cents first?

Open richardaum opened this issue 6 years ago • 22 comments

Consider the following input value: 12345

The output would be: $ 123.45 instead of $ 1,2345.00.

How to achieve the first output using this library?

This is how I am using right now, unfortunately I couldn't make it work as desired:

<NumberFormat
  customInput={Input}
  decimalScale={2}
  decimalSeparator=","
  fixedDecimalScale
  onKeyDown={onEnter(handleEnter)}
  onValueChange={handleValueChange}
  placeholder="$ 0,00"
  prefix="$ "
  thousandSeparator="."
  value={value}
/>

richardaum avatar Oct 21 '19 01:10 richardaum

I would like to know this as well

felri avatar Oct 24 '19 23:10 felri

Both outputs you mentioned seems incorrect. It should be $ 12.345.00 as you have mentioned fixedDecimalScale. Also, don't forget to pass the isNumericString prop. If you are passing a numeric string as value.

s-yadav avatar Nov 08 '19 08:11 s-yadav

@s-yadav It depends on which locale you are referring. It is correct when using, for instance, pt-BR.

richardaum avatar Nov 09 '19 13:11 richardaum

@felri I've found out how to format the number properly - using a formatter. The trick is to divide value by 100.

export default function currencyFormatter(value) {
  if (!Number(value)) return "";

  const amount = new Intl.NumberFormat("pt-BR", {
    style: "currency",
    currency: "BRL"
  }).format(value / 100);

  return `${amount}`;
}

Then you must use this format into your component:

<NumberFormat {...allOtherRequiredProps} format={currencyFormatter} />

richardaum avatar Nov 09 '19 13:11 richardaum

@richardaum That's the trick. Thank you very much!

andrezimpel avatar Nov 09 '19 13:11 andrezimpel

@richardaum thank you, works great

felri avatar Nov 11 '19 13:11 felri

@richardaum I tried with this approach, but the values object returns float value wrongly.

For example, if I type 12345, the field shows R$ 123,45, but the values object returns {formattedValue: "R$ 123,45", value: "12345", floatValue: 12345}

Did this happen to you?

nathanmrtns avatar Nov 12 '19 20:11 nathanmrtns

Yeah. This is expected because the approach is based on a formatter only. So it will only change how the number is displayed. You must handle the float number and divide it by 100 before persisting it or whatever you do with this number.

An alternative could be if a implementation is made to make this feature native, then we could expect float number also be divide by 100 automatically.

You could also implement your own wrapper to this component and expose a float number already divided by 100.

richardaum avatar Nov 13 '19 11:11 richardaum

@felri I've found out how to format the number properly - using a formatter. The trick is to divide value by 100.

export default function currencyFormatter(value) {
  if (!Number(value)) return "";

  const amount = new Intl.NumberFormat("pt-BR", {
    style: "currency",
    currency: "BRL"
  }).format(value / 100);

  return `${amount}`;
}

Then you must use this format into your component:

<NumberFormat {...allOtherRequiredProps} format={currencyFormatter} />

Please number formatter ?

stonestecnologia avatar Jul 13 '20 05:07 stonestecnologia

@richardaum Thank you so much! (Valeu demais velho)

henrique-smr avatar Dec 07 '20 23:12 henrique-smr

To clarify, format only transforms the value in the UI.

To make sure I get the right value in onValueChange I used:

onValueChange={values => {
	if(!onValueChange) { return; }
	const val = (parseFloat(values.value) / 100).toFixed(2)
	const floatVal = values.floatValue && values.floatValue / 100
	onValueChange({value: val, floatValue: floatVal, formattedValue, values.formattedValue})
}}

So if I type 900, val === "9.00" and floatVal === 9, while values.value === "900" and values.floatValue === 900.

I also had to fix the initial value

value={String(parseFloat(value + "" ?? "0") * 100)}

+ "" is to coerce number to string, ?? "0" is to default to "0" if value is undefined.

With these changes, internally the value is still 900 but on the consumer side the value is 9.00

Additionally, I had to fix the caret positioning with

onFocus={e => {
  if (inputRef.current && value) {
    const positionLastDigit = inputRef.current.value.length - suffix.length;
    setCaretPosition(inputRef.current, positionLastDigit);
  }
  props.onFocus && props.onFocus(e);
}}

with setCaretPosition being a rip-off of https://github.com/s-yadav/react-number-format/blob/8af37958c69ff1e0252282d37e922a913e52e046/lib/utils.js#L146

roine avatar Jan 07 '21 01:01 roine

Yeah. This is expected because the approach is based on a formatter only. So it will only change how the number is displayed. You must handle the float number and divide it by 100 before persisting it or whatever you do with this number. An alternative could be if a implementation is made to make this feature native, then we could expect float number also be divide by 100 automatically. You could also implement your own wrapper to this component and expose a float number already divided by 100.

Thats works perfectly for me, also the other solution. Many thanks.

Also you need to fix setting the value externally and returning the right value as said here:

To clarify, format only transforms the value in the UI.

To make sure I get the right value in onValueChange I used:

onValueChange={values => {
	if(!onValueChange) { return; }
	const val = (parseFloat(values.value) / 100).toFixed(2)
	const floatVal = values.floatValue && values.floatValue / 100
	onValueChange({value: val, floatValue: floatVal, formattedValue, values.formattedValue})
}}

So if I type 900, val === "9.00" and floatVal === 9, while values.value === "900" and values.floatValue === 900.

I also had to fix the initial value

value={String(parseFloat(value + "" ?? "0") * 100)}

+ "" is to coerce number to string, ?? "0" is to default to "0" if value is undefined.

With these changes, internally the value is still 900 but on the consumer side the value is 9.00

Additionally, I had to fix the caret positioning with

onFocus={e => {
  if (inputRef.current && value) {
    const positionLastDigit = inputRef.current.value.length - suffix.length;
    setCaretPosition(inputRef.current, positionLastDigit);
  }
  props.onFocus && props.onFocus(e);
}}

with setCaretPosition being a rip-off of

https://github.com/s-yadav/react-number-format/blob/8af37958c69ff1e0252282d37e922a913e52e046/lib/utils.js#L146

NicolasZim avatar Jan 07 '21 17:01 NicolasZim

I came up with another (simpler?) solution

<NumberFormat
  fixedDecimalScale
  decimalScale={2}
  allowNegative={false}
  value={Number(value) / 100}
  // dividing by 100 to avoid an infinite loop;
  // value cannot be changed by onValueChange because it would trigger onValueChange which would trigger onValueChange which would trigger onValueChange which would trigger onValueChange...
  onValueChange={(e) => {
    if (e.value === '') onChangeHandler(0)
    else onChangeHandler(parseFloat(e.value) * 100)
  }}
/>

I'm multiplying by 100 inside onValueChange to save cents, and also dividing the linked value by 100. Not battle tested yet, but no infinite loops so far + the formatting I want.

FelDev avatar Jun 24 '21 16:06 FelDev

Appreciate all the info on this issue guys. I've managed to create my own solution.

I didn't have time yet to optimize this code, but I think it can help someone.

I'm using Material UI + Typescript, and I created this InputCurrency component:

import React from 'react';
import NumberFormat from 'react-number-format';

import { TextField, TextFieldProps } from '@material-ui/core';

function currencyFormatter(value: any) {
  if (!Number(value)) return '';

  const amount = new Intl.NumberFormat('pt-BR', {
    style: 'currency',
    currency: 'BRL',
  }).format(value / 100);

  return `${amount}`;
}

// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
function NumberFormatCustom({ inputRef, onChange, value, ...props }: any) {
  const [internalValue, setInternalValue] = React.useState('');

  return (
    <NumberFormat
      value={internalValue}
      onValueChange={(values) => {
        setInternalValue(values.value);
        onChange({
          target: {
            name: props.name,
            value: String(Number(values.value) / 100),
          },
        });
      }}
      getInputRef={inputRef}
      format={currencyFormatter}
      thousandSeparator="."
      decimalSeparator=","
      isNumericString
      {...props}
    />
  );
}

const InputCurrencyBase: React.ForwardRefRenderFunction<any, TextFieldProps> = (
  props,
  ref
) => {
  const intl = useIntl();

  return (
      <TextField
        variant="outlined"
        color="primary"
        InputProps={{
          inputComponent: NumberFormatCustom as any,
        }}
        autoComplete="off"
        inputRef={ref}
        {...props}
      />
  );
};

const InputCurrency = React.forwardRef(InputCurrencyBase);

export default InputCurrency;

You can use it like a normal TextField of Material UI:

const [inputValue, setInputValue] = useState('');

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  const { value } = event.target; // value comes like "1475.85", number parseable
  setInputValue(value);
};

return <InputCurrency value={inputValue} onChange={handleChange} />;

juanvl avatar Jul 22 '21 19:07 juanvl

I found a way with a workaround. Maybe it can help somebody: https://codesandbox.io/s/number-format-input-ey95k

gehres avatar Jul 25 '21 18:07 gehres

adding fixedDecimalScale to my code worked for me, Thank you @richardaum

<NumericFormat {...field} variant="outlined" label="Amount" fullWidth error={invalid} customInput={TextField} prefix={'£ '} decimalScale={2} thousandSeparator=" " fixedDecimalScale />

Consider the following input value: 12345

The output would be: $ 123.45 instead of $ 1,2345.00.

How to achieve the first output using this library?

This is how I am using right now, unfortunately I couldn't make it work as desired:

<NumberFormat
  customInput={Input}
  decimalScale={2}
  decimalSeparator=","
  fixedDecimalScale
  onKeyDown={onEnter(handleEnter)}
  onValueChange={handleValueChange}
  placeholder="$ 0,00"
  prefix="$ "
  thousandSeparator="."
  value={value}
/>

Harishan15 avatar Oct 29 '22 11:10 Harishan15

Screenshot (197) Hi all, please I have a problem solving this issue using the CurrencyFormat. Unwanted input appeared showing the total price of items inside. please how can i prevent the input from appearing.

DanAhhmad avatar Mar 06 '23 01:03 DanAhhmad

Screenshot (198) here is the code pls can any one help?

DanAhhmad avatar Mar 06 '23 01:03 DanAhhmad

I came across same problem and have found that the solution proposed by @richardaum does not work with latest version of the library. Probably it could work with some code changes, but also I have found that it would introduce some complexity to the project. Then I found a way to accomplish the task with plain JavaScript/React:

<input
  value={value}
  onChange={(event) => {
    let value = event.target.value.replace(/[^0-9]/g, '');
    value = "R$ " + (Number(value) / 100).toFixed(2);
    setValue(value);
  }} 
/>

marciosouzajunior avatar Jun 07 '23 18:06 marciosouzajunior

After some time, this library has changed its interfaces. Here's is the update code using NumberFormatBase:

import { NumberFormatBase } from "react-number-format";

function currencyFormatter(value: string) {
  if (!value) return "";

  const number = Number(value);
  const amount = new Intl.NumberFormat("pt-BR", {
    style: "currency",
    currency: "BRL",
  }).format(number / 100);

  return `${amount}`;
}

<NumberFormatBase format={currencyFormatter} prefix={"R$ "} />

richardaum avatar Feb 03 '24 16:02 richardaum

Thank you @richardaum. Do you know if there is a way to allow negative numbers in your approach?

byanes avatar Feb 27 '24 18:02 byanes

Thank you @richardaum. Do you know if there is a way to allow negative numbers in your approach?

I did it this way

const [negative, setNegative] = useState(false);

function currencyFormatter(value) {
if (!Number(value)) return '';

if (negative) {
  value = -parseFloat(value);
}
const amount = new Intl.NumberFormat('pt-BR', {
  style: 'currency',
  currency: 'BRL',
}).format(value / 100);

return `${amount}`;
}

<NumberFormatBase
        format={currencyFormatter}
        onKeyDown={(e) => {
          const el = e.target;
          const {key} = e;
          if (key === '-') {
            setNegative(!negative);
            e.preventDefault();
            return;
          }
        }}
      />

coppibruno avatar Apr 11 '24 16:04 coppibruno