Skip to content

Commit

Permalink
Merge branch 'master' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
aashu16 committed Sep 9, 2023
2 parents 651faa5 + 71b318d commit 825c87d
Show file tree
Hide file tree
Showing 11 changed files with 546 additions and 47 deletions.
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# react-number-format

React Number Format is an input-formatter library with a sophisticated and light weight caret engine. It ensures that a user can only enter text that meets specific numeric or string patterns, and formats the input value for display.
React Number Format is an input-formatter library with a sophisticated and light weight caret engine. It ensures that a user can only enter text that meets specific numeric or string patterns, and formats the input value for display.

### Features

Expand Down Expand Up @@ -33,6 +33,11 @@ Using `yarn`
yarn add react-number-format
```

### Documentation

Read the full documentation here
[https://s-yadav.github.io/react-number-format/docs/intro](https://s-yadav.github.io/react-number-format/docs/intro)

#### ES6

Numeric Format
Expand All @@ -41,15 +46,15 @@ Numeric Format
import { NumericFormat } from 'react-number-format';
```

NumericFormat Props: [https://s-yadav.github.io/react-number-format/docs/numeric_format](https://s-yadav.github.io/react-number-format/docs/numeric_format)

Pattern Format

```js
import { PatternFormat } from 'react-number-format';
```

Read the full documentation here

[https://s-yadav.github.io/react-number-format/docs/intro](https://s-yadav.github.io/react-number-format/docs/intro)
PatternFormat Props: [https://s-yadav.github.io/react-number-format/docs/pattern_format](https://s-yadav.github.io/react-number-format/docs/pattern_format)

### Migrate from v4 to v5

Expand Down
197 changes: 189 additions & 8 deletions documentation/v5/docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ React Number Format v5 is a complete rewrite with a goal of keeping it fully cus

The primary thing which react number format controls is apply formatting in place (in the input) while managing correct caret position. It tries to understand what user is trying to do, add number, cut/paste, delete, and manage cursor position accordingly.

At the core of React number format lies NumberFormatBase, which works on four main props controlled from parent.
At the core of React number format lies NumberFormatBase, which works on three main props controlled from parent.

- **format**: A format function which can turn any numeric string to a formatted string.
- **removeFormatting**: A function to removing formatting from a formatted string and return numeric string.
- **isValidInputCharacter**: A function to tell if a character in the formatted value is a valid typeable character. You don't need to pass it most of the time, as it defaults numeric characters (0-9). But case like additional character is allowed to type, for example decimal separator in currency format.
- **getCaretBoundary**: A function given a formatted string, returns boundaries of valid cursor position. basically an array of boolean, where index of specify caret position. true at a index signifies user can put their caret at the position, false means the caret position is not allowed and the caret will move to closet allowed position.
- **format** `(numStr: string) => string`: A format function which can turn any numeric string to a formatted string.
- **removeFormatting** `(formattedStr: string) => string`: A function to removing formatting from a formatted string and return numeric string.
- **getCaretBoundary** `(formattedStr: string) => boolean[]`: A function given a formatted string, returns boundaries of valid cursor position. basically an array of boolean, where index of specify caret position. true at a index signifies user can put their caret at the position, false means the caret position is not allowed and the caret will move to closet allowed position.

Most of the time you don't have to define getCaretBoundary, as the default one is enough, but in case you need to define, it looks something like this.

Expand All @@ -33,6 +32,23 @@ function caretUnknownFormatBoundary(formattedValue) {
}
```

There are few more props to handle some corner case.

- **isValidInputCharacter** `(char: sting) => boolean`: A function to tell if a character in the formatted value is a valid typeable character. You don't need to pass it most of the time, as it defaults numeric characters (0-9). But case like additional character is allowed to type, for example decimal separator in currency format.
- **isCharacterSame** `(compareProps: CompareProps) => boolean`: Some time we would like to allow user pressing different key and that being interpreted as different key like custom numerals, or letting user press `.` for decimal separator when custom decimalSeparator is provided. In such case we need to inform the library that the two characters are same.

```js
type CompareProps = {
currentValue: string, // current value in the input, before applying any formatting
lastValue: string, // last formatted value
formattedValue: string, // current formatted value.
currentValueIndex: number, // character index in currentValue which we are comparing
formattedValueIndex: number, // character index in formattedValue which we are comparing
};
```

Check the usage in [custom numeral example](#custom-numeral-example).

Apart from this prop some key handling are required depending on use case which can be done using native events, onKeyDown/onKeyUp etc.

## Examples
Expand Down Expand Up @@ -170,7 +186,7 @@ Another example for NumericFormat could be support for custom numerals.
const persianNumeral = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];

function CustomNumeralNumericFormat(props) {
const { format, removeFormatting, ...rest } = useNumericFormat(props);
const { format, removeFormatting, isCharacterSame, ...rest } = useNumericFormat(props);

const _format = (val) => {
const _val = format(val);
Expand All @@ -185,7 +201,25 @@ function CustomNumeralNumericFormat(props) {
return removeFormatting(_val);
};

return <NumberFormatBase format={_format} removeFormatting={_removeFormatting} {...rest} />;
const _isCharacterSame = (compareMeta) => {
const isCharSame = isCharacterSame(compareMeta);
const { formattedValue, currentValue, formattedValueIndex, currentValueIndex } = compareMeta;
const curChar = currentValue[currentValueIndex];
const newChar = formattedValue[formattedValueIndex];
const curPersianChar = persianNumeral[Number(curChar)] ?? curChar;
const newPersianChar = persianNumeral[Number(newChar)] ?? newChar;

return isCharSame || curPersianChar || newPersianChar;
};

return (
<NumberFormatBase
format={_format}
removeFormatting={_removeFormatting}
isCharacterSame={_isCharacterSame}
{...rest}
/>
);
}
```
Expand All @@ -194,7 +228,154 @@ function CustomNumeralNumericFormat(props) {
Demo
</summary>
<iframe src="https://codesandbox.io/embed/custom-numeral-numer-format-forked-s8e1s4?fontsize=14&hidenavigation=1&theme=dark&view=preview"
title="Custom Numeral (Numer Format)"
title="Custom numeral example"
className="csb"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>
</details>
### AllowEmptyFormatting on NumericFormat
Currently allowEmptyFormatting is only available on the pattern lock, while it isn't a common usecase in NumericFormat, you still might want that behavior, you can achieve it like following.
```js
function CustomNumberFormat(props) {
const { prefix = '', suffix = '', allowEmptyFormatting } = props;
const { format, ...numberFormatBaseProps } = useNumericFormat(props);
const _format = (numStr, props) => {
const formattedValue = format(numStr, props);
return allowEmptyFormatting && formattedValue === '' ? prefix + suffix : formattedValue;
};

return <NumberFormatBase {...numberFormatBaseProps} format={_format} />;
}
```
<details>
<summary>
Demo
</summary>
<iframe src="https://codesandbox.io/embed/numeric-format-allowemptyformat-zt3mh8?fontsize=14&hidenavigation=1&theme=dark&view=preview"
title="AllowEmptyFormatting on NumericFormat"
className="csb"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>
</details>
### Using parentheses to express negative numbers
In some financial application we may want to express negative numbers enclosed with parentheses `($111,222)` as opposed to negative sign ahead of the number `-$111,222`. This can be implemented outside of the lib since v5.
```js
const NEGATION_FORMAT_REGEX = /^\((.*)\)$/;

function extractNegationAndNumber(value) {
let hasNegation = false;
if (typeof value === 'number') {
hasNegation = value < 0;
value = hasNegation ? value * -1 : value;
} else if (value?.[0] === '-') {
hasNegation = true;
value = value.substring(1);
} else if (value?.match(NEGATION_FORMAT_REGEX)) {
hasNegation = true;
value = value.replace(NEGATION_FORMAT_REGEX, '$1');
}

return { hasNegation, value };
}

function CustomNegationNumberFormat({
prefix = '',
suffix = '',
value,
defaultValue,
onValueChange,
...restProps
}) {
const [hasNegation, toggleNegation] = useState(
extractNegationAndNumber(value ?? defaultValue).hasNegation,
);
const [internalValue, setInternalValue] = useState(
extractNegationAndNumber(value ?? defaultValue).value,
);
useEffect(() => {
const { hasNegation, value: internalValue } = extractNegationAndNumber(value);
setInternalValue(internalValue);
toggleNegation(hasNegation);
}, [value]);

const _onValueChange = (values, sourceInfo) => {
if (!onValueChange) return;

const { formattedValue, value, floatValue } = values;
onValueChange(
{
formattedValue,
value: hasNegation ? `-${value}` : value,
floatValue: hasNegation && !isNaN(floatValue) ? -floatValue : floatValue,
},
sourceInfo,
);
};

const props = {
prefix: hasNegation ? '(' + prefix : prefix,
suffix: hasNegation ? suffix + ')' : suffix,
// as we are controlling the negation logic outside, we don't want numeric format to handle this
allowNegative: false,
value: internalValue,
onValueChange: _onValueChange,
...restProps,
};
const { format, onKeyDown, ...numberFormatBaseProps } = useNumericFormat(props);

const _format = (numStr) => {
const formattedValue = format(numStr, props);
// if negation is present we need to always show negation with prefix and suffix even if value is empty
return formattedValue === '' && hasNegation ? props.prefix + props.suffix : formattedValue;
};

const _onKeyDown = (e) => {
const el = e.target;
const { key } = e;
const { selectionStart, selectionEnd, value = '' } = el;

// if every thing is selected and deleted remove the negation as well
if (selectionStart !== selectionEnd) {
// if multiple characters are selected and user hits backspace, no need to handle anything manually
onKeyDown(e);
return;
}

// if user is pressing '-' we want to change it to '()', so mark there is negation in the number
if (key === '-') {
toggleNegation((hasNegation) => !hasNegation);
e.preventDefault();
return;
}

if (key === 'Backspace' && value[0] === '(' && selectionStart === props.prefix.length) {
toggleNegation(false);
e.preventDefault();
return;
}

onKeyDown(e);
};

return <NumberFormatBase {...numberFormatBaseProps} onKeyDown={_onKeyDown} format={_format} />;
}
```
<details>
<summary>
Demo
</summary>
<iframe src="https://codesandbox.io/embed/parentheses-for-negation-forked-jn42cp?fontsize=14&hidenavigation=1&theme=dark&view=preview"
title="Using parentheses to express negative numbers"
className="csb"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
Expand Down
6 changes: 4 additions & 2 deletions documentation/v5/docs/props.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { TextField } from '@mui/material';
<NumericFormat value={12323} customInput={TextField} />;
```

**Note**: customInput expects reference of component (not a render prop), if you pass an inline component like this `<NumericFormat customInput={() => <TextField />} />`, it will not work.

<details>
<summary>
Demo
Expand Down Expand Up @@ -160,7 +162,7 @@ const MAX_LIMIT = 1000;

**default**: false

If value is passed as string representation of numbers (unformatted) and number is used in any format props like in prefix or suffix in numeric format and format prop in pattern format then this should be passed as `true`.
If value is passed as string representation of numbers (unformatted) and thousandSeparator is `.` in numeric format or number is used in any format props like in prefix or suffix in numeric format and format prop in pattern format then this should be passed as `true`.

**Note**: Prior to 5.2.0 its was always required to be passed as true when value is passed as string representation of numbers (unformatted).

Expand Down Expand Up @@ -190,7 +192,7 @@ import { PatternFormat } from 'react-number-format';
This handler provides access to any values changes in the input field and is triggered only when a prop changes or the user input changes. It provides two arguments namely the [valueObject](quirks#values-object) as the first and the [sourceInfo](quirks#sourceInfo) as the second. The [valueObject](quirks#values-object) parameter contains the `formattedValue`, `value` and the `floatValue` of the given input field. The [sourceInfo](quirks#sourceInfo) contains the `event` Object and a `source` key which indicates whether the triggered change is due to an event or a prop change. This is particularly useful in identify whether the change is user driven or is an uncontrolled change due to any prop value being updated.

:::info
If you are using `values.value` which is non formatted value as numeric string. Make sure to pass valueIsNumericString to be true if any of the format prop has number on it. See [valueIsNumericString](#valueisnumericstring-boolean) for more details.
If you are using `values.value` which is non formatted value as numeric string. Make sure to pass valueIsNumericString to be true if any of the format prop has number on it, or if thousandSeparator is `.` in NumericFormat . See [valueIsNumericString](#valueisnumericstring-boolean) for more details.
:::

```js
Expand Down
8 changes: 6 additions & 2 deletions documentation/v5/docs/quirks.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ values object is on following format
}
```

Its recommended to use formattedValue / value / floatValue based on the initial state (it should be same as the initial state format) which you are passing as value prop. If you are saving the `value` key on state make sure to pass valueIsNumericString prop to true.
Its recommended to use formattedValue / value / floatValue based on the initial state (it should be same as the initial state format) which you are passing as value prop. If you are saving the `value` key on state and any of the format prop like prefix/suffix contains number make sure to pass valueIsNumericString prop to true.

### Notes and quirks

1. Value can be passed as string or number, but if it is passed as string it should be either formatted value or if it is a numeric string, you have to set valueIsNumericString props to true.
1. Value can be passed as string or number, but if it is passed as string it should be either formatted value or if it is a numeric string and any of the format prop like prefix/suffix contains number, you have to set valueIsNumericString props to true.

2. Value as prop will be rounded to given decimal scale if format option is not provided.

Expand All @@ -35,6 +35,10 @@ Its recommended to use formattedValue / value / floatValue based on the initial

7. onValueChange is not same as onChange. It gets called on whenever there is change in value which can be caused by any event like change or blur event or by a prop change. It also provides a second argument which contains the event object and the reason for this function trigger.

8. `minLength` and `maxLength` prop of native input doesn't work as expected, as the formatting happens post the number is added on the input. You can achieve similar result using isAllowed prop.

Related issue: https://github.com/s-yadav/react-number-format/issues/758

## SourceInfo object

The `sourceInfo` object indicates whether the triggered change is due to an event or a prop change. This is particularly useful in identify whether the change is user driven or is an uncontrolled change due to any prop value being updated.
Expand Down
Loading

0 comments on commit 825c87d

Please sign in to comment.