From 1d1171f06d52aaa6ea19bcffe29fbc9e0c026bbb Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sat, 5 Aug 2023 15:55:30 +0530 Subject: [PATCH 01/10] - Update readme for linking the documentation in better way. - Added allowEmptyFormatting prop customization doc for NumericFormat --- README.md | 13 ++++++++---- documentation/v5/docs/customization.md | 29 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7d3304ce..17c174d1 100755 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 diff --git a/documentation/v5/docs/customization.md b/documentation/v5/docs/customization.md index 2f0bc552..0d3d1729 100644 --- a/documentation/v5/docs/customization.md +++ b/documentation/v5/docs/customization.md @@ -200,3 +200,32 @@ function CustomNumeralNumericFormat(props) { sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" > + +### 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 ; +} +``` + +
+ + Demo + + +
From e502b2cff3b1b5cf6415fa7ab9b016e6d279cd48 Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sun, 13 Aug 2023 22:02:15 +0530 Subject: [PATCH 02/10] - Fix #774 - Fix custom numeral example - Added support to inform rnf to treat two character as same. --- documentation/v5/docs/customization.md | 48 ++++++++++++++++--- example/src/index.js | 51 ++++++++++++++++++-- src/number_format_base.tsx | 4 +- src/numeric_format.tsx | 42 ++++++++++++++-- src/types.ts | 9 ++++ src/utils.tsx | 64 ++++++++++++++++++++----- test/library/keypress_and_caret.spec.js | 41 +++++++++++++++- 7 files changed, 230 insertions(+), 29 deletions(-) diff --git a/documentation/v5/docs/customization.md b/documentation/v5/docs/customization.md index 0d3d1729..c1840ae3 100644 --- a/documentation/v5/docs/customization.md +++ b/documentation/v5/docs/customization.md @@ -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. @@ -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 @@ -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); @@ -185,7 +201,25 @@ function CustomNumeralNumericFormat(props) { return removeFormatting(_val); }; - return ; + 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 ( + + ); } ``` diff --git a/example/src/index.js b/example/src/index.js index 17e7402c..707400bd 100644 --- a/example/src/index.js +++ b/example/src/index.js @@ -1,12 +1,51 @@ import React from 'react'; import ReactDOM from 'react-dom'; import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; -import NumericFormat from '../../src/numeric_format'; +import NumericFormat, { useNumericFormat } from '../../src/numeric_format'; import PatternFormat from '../../src/pattern_format'; import NumberFormatBase from '../../src/number_format_base'; import TextField from 'material-ui/TextField'; import { cardExpiry } from '../../custom_formatters/card_expiry'; +const persianNumeral = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']; + +function CustomNumeralNumericFormat(props) { + const { format, removeFormatting, isCharacterSame, ...rest } = useNumericFormat(props); + + const _format = (val) => { + const _val = format(val); + return _val.replace(/\d/g, ($1) => persianNumeral[Number($1)]); + }; + + const _removeFormatting = (val, ...rest) => { + const _val = val.replace(new RegExp(persianNumeral.join('|'), 'g'), ($1) => + persianNumeral.indexOf($1), + ); + + return removeFormatting(_val, ...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 ( + + ); +} + class App extends React.Component { constructor() { super(); @@ -84,7 +123,8 @@ class App extends React.Component {

Custom thousand separator : Format currency in input

ThousandSeparator: '.', decimalSeparator=','
- + , +

ThousandSeparator: ' ', decimalSeparator='.'
@@ -152,7 +192,12 @@ class App extends React.Component {

Custom Numeral: add support for custom languages

- +
); diff --git a/src/number_format_base.tsx b/src/number_format_base.tsx index 808ec488..f8d1e52b 100644 --- a/src/number_format_base.tsx +++ b/src/number_format_base.tsx @@ -50,6 +50,7 @@ export default function NumberFormatBase( value: propValue, getCaretBoundary = caretUnknownFormatBoundary, isValidInputCharacter = charIsNumber, + isCharacterSame, ...otherProps } = props; @@ -135,6 +136,7 @@ export default function NumberFormatBase( caretPos, caretBoundary, isValidInputCharacter, + isCharacterSame, ); //correct caret position if its outside of editable area @@ -230,7 +232,7 @@ export default function NumberFormatBase( : undefined; // needed to prevent warning with useLayoutEffect on server - const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect + const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; useIsomorphicLayoutEffect(() => { const input = focusedElm.current; diff --git a/src/numeric_format.tsx b/src/numeric_format.tsx index 8dd94d7b..c3e2416e 100644 --- a/src/numeric_format.tsx +++ b/src/numeric_format.tsx @@ -14,6 +14,7 @@ import { toNumericString, charIsNumber, isNotValidValue, + findChangeRange, } from './utils'; import { NumericFormatProps, @@ -23,6 +24,7 @@ import { FormatInputValueFunction, RemoveFormattingFunction, NumberFormatBaseProps, + IsCharacterSame, } from './types'; import NumberFormatBase from './number_format_base'; @@ -334,9 +336,9 @@ export function useNumericFormat( props = validateAndUpdateProps(props); const { - decimalSeparator = '.', /* eslint-disable no-unused-vars */ - allowedDecimalSeparators, + decimalSeparator: _decimalSeparator, + allowedDecimalSeparators: _allowedDecimalSeparators, thousandsGroupStyle, suffix, allowNegative, @@ -355,6 +357,9 @@ export function useNumericFormat( ...restProps } = props; + // get derived decimalSeparator and allowedDecimalSeparators + const { decimalSeparator, allowedDecimalSeparators } = getSeparators(props); + const _format: FormatInputValueFunction = (numStr) => format(numStr, props); const _removeFormatting: RemoveFormattingFunction = (inputValue, changeMeta) => @@ -421,7 +426,6 @@ export function useNumericFormat( } // don't allow user to delete decimal separator when decimalScale and fixedDecimalScale is set - const { decimalSeparator, allowedDecimalSeparators } = getSeparators(props); if ( key === 'Backspace' && value[selectionStart - 1] === decimalSeparator && @@ -492,11 +496,43 @@ export function useNumericFormat( return charIsNumber(inputChar); }; + const isCharacterSame: IsCharacterSame = ({ + currentValue, + lastValue, + formattedValue, + currentValueIndex, + formattedValueIndex, + }) => { + const curChar = currentValue[currentValueIndex]; + const newChar = formattedValue[formattedValueIndex]; + + /** + * NOTE: as thousand separator and allowedDecimalSeparators can be same, we need to check on + * typed range if we have typed any character from allowedDecimalSeparators, in that case we + * consider different characters like , and . same within the range of updated value. + */ + const typedRange = findChangeRange(lastValue, currentValue); + const { to } = typedRange; + + if ( + currentValueIndex >= to.start && + currentValueIndex < to.end && + allowedDecimalSeparators && + allowedDecimalSeparators.includes(curChar) && + newChar === decimalSeparator + ) { + return true; + } + + return curChar === newChar; + }; + return { ...(restProps as NumberFormatBaseProps), value: formattedValue, valueIsNumericString: false, isValidInputCharacter, + isCharacterSame, onValueChange: _onValueChange, format: _format, removeFormatting: _removeFormatting, diff --git a/src/types.ts b/src/types.ts index 3d162e7e..d47d8539 100644 --- a/src/types.ts +++ b/src/types.ts @@ -57,6 +57,14 @@ type NumberFormatProps = Props & export type OnValueChange = (values: NumberFormatValues, sourceInfo: SourceInfo) => void; +export type IsCharacterSame = (compareProps: { + currentValue: string; + lastValue: string; + formattedValue: string; + currentValueIndex: number; + formattedValueIndex: number; +}) => boolean; + type NumberFormatBase = { type?: 'text' | 'tel' | 'password'; displayType?: 'input' | 'text'; @@ -77,6 +85,7 @@ type NumberFormatBase = { onBlur?: InputAttributes['onBlur']; getCaretBoundary?: (formattedValue: string) => boolean[]; isValidInputCharacter?: (character: string) => boolean; + isCharacterSame?: IsCharacterSame; }; export type NumberFormatBaseProps = NumberFormatProps< diff --git a/src/utils.tsx b/src/utils.tsx index 6d14fee1..b6ac369b 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -1,5 +1,10 @@ import { useMemo, useRef, useState } from 'react'; -import { NumberFormatBaseProps, FormatInputValueFunction, OnValueChange } from './types'; +import { + NumberFormatBaseProps, + FormatInputValueFunction, + OnValueChange, + IsCharacterSame, +} from './types'; // basic noop function export function noop() {} @@ -7,6 +12,23 @@ export function returnTrue() { return true; } +export function memoizeOnce(cb: (...args: T) => R) { + let lastArgs: T | undefined; + let lastValue: R = undefined; + return (...args: T) => { + if ( + lastArgs && + args.length === lastArgs.length && + args.every((value, index) => value === lastArgs[index]) + ) { + return lastValue; + } + lastArgs = args; + lastValue = cb(...args); + return lastValue; + }; +} + export function charIsNumber(char?: string) { return !!(char || '').match(/\d/); } @@ -243,7 +265,7 @@ export function findChangedIndex(prevValue: string, newValue: string) { return { start: i, end: prevLength - j }; } -export function findChangeRange(prevValue: string, newValue: string) { +export const findChangeRange = memoizeOnce((prevValue: string, newValue: string) => { let i = 0, j = 0; const prevLength = prevValue.length; @@ -263,7 +285,7 @@ export function findChangeRange(prevValue: string, newValue: string) { from: { start: i, end: prevLength - j }, to: { start: i, end: newLength - j }, }; -} +}); /* Returns a number whose value is limited to the given range @@ -306,6 +328,15 @@ export function getMaskAtIndex(mask: string | string[] = ' ', index: number) { return mask[index] || ' '; } +function defaultIsCharacterSame({ + currentValue, + formattedValue, + currentValueIndex, + formattedValueIndex, +}: Parameters[0]) { + return currentValue[currentValueIndex] === formattedValue[formattedValueIndex]; +} + export function getCaretPosition( newFormattedValue: string, lastFormattedValue: string, @@ -313,16 +344,14 @@ export function getCaretPosition( curCaretPos: number, boundary: boolean[], isValidInputCharacter: (char: string) => boolean, + /** + * format function can change the character, the caret engine relies on mapping old value and new value + * In such case if character is changed, parent can tell which chars are equivalent + * Some example, all allowedDecimalCharacters are updated to decimalCharacters, 2nd case if user is coverting + * number to different numeric system. + */ + isCharacterSame: IsCharacterSame = defaultIsCharacterSame, ) { - const changeRange = findChangeRange(curValue, newFormattedValue); - const { from, to } = changeRange; - - // if only last typed character is changed in the - if (from.end - from.start === 1 && from.end === to.end && to.end === curCaretPos) { - // don't do anything - return curCaretPos; - } - /** * if something got inserted on empty value, add the formatted character before the current value, * This is to avoid the case where typed character is present on format characters @@ -330,6 +359,7 @@ export function getCaretPosition( const firstAllowedPosition = boundary.findIndex((b) => b); const prefixFormat = newFormattedValue.slice(0, firstAllowedPosition); if (!lastFormattedValue && !curValue.startsWith(prefixFormat)) { + lastFormattedValue = prefixFormat; curValue = prefixFormat + curValue; curCaretPos = curCaretPos + prefixFormat.length; } @@ -344,7 +374,15 @@ export function getCaretPosition( for (let i = 0; i < curValLn; i++) { indexMap[i] = -1; for (let j = 0, jLn = formattedValueLn; j < jLn; j++) { - if (curValue[i] === newFormattedValue[j] && addedIndexMap[j] !== true) { + const isCharSame = isCharacterSame({ + currentValue: curValue, + lastValue: lastFormattedValue, + formattedValue: newFormattedValue, + currentValueIndex: i, + formattedValueIndex: j, + }); + + if (isCharSame && addedIndexMap[j] !== true) { indexMap[i] = j; addedIndexMap[j] = true; break; diff --git a/test/library/keypress_and_caret.spec.js b/test/library/keypress_and_caret.spec.js index 5af742d6..93af0482 100644 --- a/test/library/keypress_and_caret.spec.js +++ b/test/library/keypress_and_caret.spec.js @@ -133,7 +133,23 @@ describe('Test keypress and caret position changes', () => { it('should handle caret position correctly when suffix starts with space and allowed decimal separator is pressed. #725', async () => { const { input } = await render( , + ); + + simulateNativeKeyInput(input, '.', 2, 2); + expect(input.selectionStart).toEqual(3); + }); + + it('should handle caret position correctly when suffix starts with space and allowed decimal separator is pressed in empty input. #774', async () => { + const { input } = await render( + { />, ); - simulateNativeKeyInput(input, '.', 1, 1); + simulateNativeKeyInput(input, '.', 0, 0); + expect(input.selectionStart).toEqual(1); + }); + + it('should handle the caret position when prefix is provided and number is entered on empty input', async () => { + const { input } = await render(); + + simulateNativeKeyInput(input, '1', 0, 0); + expect(input.selectionStart).toEqual(2); + }); + + it('should handle the caret position when prefix is provided and allowed decimal separator is entered on empty input', async () => { + const { input } = await render( + , + ); + + simulateNativeKeyInput(input, '.', 0, 0); expect(input.selectionStart).toEqual(2); }); From 9e0fd53942376892dcf59b70f32f8db213a55097 Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sun, 13 Aug 2023 22:02:53 +0530 Subject: [PATCH 03/10] Version upgrade for release. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ce4dbca..e78a4cd0 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-number-format", "description": "React component to format number in an input or as a text.", - "version": "5.2.3", + "version": "5.3.0", "main": "dist/react-number-format.cjs.js", "module": "dist/react-number-format.es.js", "types": "types/index.d.ts", From 1100ad79b3533fc2b1ce36b670b4066a565d252e Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sun, 13 Aug 2023 22:24:42 +0530 Subject: [PATCH 04/10] Add comment on doc to not use customInput as render prop. --- documentation/v5/docs/props.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/v5/docs/props.md b/documentation/v5/docs/props.md index 4b769934..8cdbd9ae 100644 --- a/documentation/v5/docs/props.md +++ b/documentation/v5/docs/props.md @@ -18,6 +18,8 @@ import { TextField } from '@mui/material'; ; ``` +**Note**: customInput expects reference of component (not a render prop), if you pass an inline component like this ` } />`, it will not work. +
Demo From 5565aa8fd7b6918b81161f1480e53c27486b44a4 Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sun, 13 Aug 2023 23:33:30 +0530 Subject: [PATCH 05/10] Add note for minValue and maxValue --- documentation/v5/docs/quirks.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/documentation/v5/docs/quirks.md b/documentation/v5/docs/quirks.md index bddb7628..7a87cfe6 100644 --- a/documentation/v5/docs/quirks.md +++ b/documentation/v5/docs/quirks.md @@ -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. @@ -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. From 424c75029c23838b1ac95007eb190bbafc3a2957 Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sun, 3 Sep 2023 20:56:01 +0530 Subject: [PATCH 06/10] - Fix infinite rerender when values.value is used valueIsNumericString not provided. #786 --- documentation/v5/docs/props.md | 4 +-- src/numeric_format.tsx | 4 +-- test/library/input_numeric_format.spec.js | 39 ++++++++++++++++++++++- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/documentation/v5/docs/props.md b/documentation/v5/docs/props.md index 8cdbd9ae..a75b793c 100644 --- a/documentation/v5/docs/props.md +++ b/documentation/v5/docs/props.md @@ -162,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). @@ -192,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 diff --git a/src/numeric_format.tsx b/src/numeric_format.tsx index c3e2416e..aa35cc98 100644 --- a/src/numeric_format.tsx +++ b/src/numeric_format.tsx @@ -371,9 +371,9 @@ export function useNumericFormat( let _valueIsNumericString = valueIsNumericString ?? isNumericString(_value, prefix, suffix); if (!isNil(value)) { - _valueIsNumericString = valueIsNumericString || typeof value === 'number'; + _valueIsNumericString = _valueIsNumericString || typeof value === 'number'; } else if (!isNil(defaultValue)) { - _valueIsNumericString = valueIsNumericString || typeof defaultValue === 'number'; + _valueIsNumericString = _valueIsNumericString || typeof defaultValue === 'number'; } const roundIncomingValueToPrecision = (value: string | number | null | undefined) => { diff --git a/test/library/input_numeric_format.spec.js b/test/library/input_numeric_format.spec.js index 04d09b55..ff6278b1 100644 --- a/test/library/input_numeric_format.spec.js +++ b/test/library/input_numeric_format.spec.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import ReactDOM from 'react-dom'; import NumericFormat from '../../src/numeric_format'; @@ -694,6 +694,43 @@ describe('Test NumberFormat as input with numeric format options', () => { expect(input.value).toEqual('12,345'); }); + it('should not infinite rerender when valueIsNumericString is not set and decimalScale is provided, and values.value is used inside onValueChange #786', async () => { + const ControlledComponent = (props) => { + const [value, setValue] = useState(''); + const [renderCount, setRenderCount] = useState(0); + + return ( + <> + { + //return to avoid infinite rerender + if (renderCount > 10) return; + setValue(values.value); + setRenderCount(renderCount + 1); + }} + {...props} + /> + {renderCount} + + ); + }; + const { input, view } = await render( + , + ); + + simulateNativeKeyInput(input, '2', 0, 0); + + const renderCount = await view.getByTestId('renderCount'); + expect(renderCount.innerHTML).toEqual('1'); + expect(input.value).toEqual('2,00'); + }); + describe('should allow typing number if prefix or suffix is just an number #691', () => { it('when prefix is number', async () => { const { input } = await render(); From dc9a0c821f8381ea78e4515c84123e707eaed16d Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sun, 3 Sep 2023 21:32:33 +0530 Subject: [PATCH 07/10] - Fix for delete of decimalSeparator happening when fixeDecimalScale is set. #789 --- src/numeric_format.tsx | 15 +++++++-------- test/library/input_numeric_format.spec.js | 10 ++++++++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/numeric_format.tsx b/src/numeric_format.tsx index aa35cc98..6daba3ca 100644 --- a/src/numeric_format.tsx +++ b/src/numeric_format.tsx @@ -426,14 +426,13 @@ export function useNumericFormat( } // don't allow user to delete decimal separator when decimalScale and fixedDecimalScale is set - if ( - key === 'Backspace' && - value[selectionStart - 1] === decimalSeparator && - decimalScale && - fixedDecimalScale - ) { - setCaretPosition(el, selectionStart - 1); - e.preventDefault(); + if (decimalScale && fixedDecimalScale) { + if (key === 'Backspace' && value[selectionStart - 1] === decimalSeparator) { + setCaretPosition(el, selectionStart - 1); + e.preventDefault(); + } else if (key === 'Delete' && value[selectionStart] === decimalSeparator) { + e.preventDefault(); + } } // if user presses the allowed decimal separator before the separator, move the cursor after the separator diff --git a/test/library/input_numeric_format.spec.js b/test/library/input_numeric_format.spec.js index ff6278b1..3a437945 100644 --- a/test/library/input_numeric_format.spec.js +++ b/test/library/input_numeric_format.spec.js @@ -731,6 +731,16 @@ describe('Test NumberFormat as input with numeric format options', () => { expect(input.value).toEqual('2,00'); }); + it('should not delete decimal separator if delete key is pressed before decimal separator when fixedDecimalScale is provided. #789', async () => { + const { input } = await render( + , + ); + + simulateNativeKeyInput(input, '{delete}', 3, 3); + + expect(input.value).toEqual('123.000'); + }); + describe('should allow typing number if prefix or suffix is just an number #691', () => { it('when prefix is number', async () => { const { input } = await render(); From ec2f10f6f8f83dd979df9c8f1db61813ee033577 Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sun, 3 Sep 2023 21:35:56 +0530 Subject: [PATCH 08/10] Release for bug fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e78a4cd0..3a2c5243 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-number-format", "description": "React component to format number in an input or as a text.", - "version": "5.3.0", + "version": "5.3.1", "main": "dist/react-number-format.cjs.js", "module": "dist/react-number-format.es.js", "types": "types/index.d.ts", From cac8b990f752242d0e0aea10646e10de97be5ef8 Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Mon, 4 Sep 2023 00:34:22 +0530 Subject: [PATCH 09/10] Add example for custom negation format --- documentation/v5/docs/customization.md | 69 +++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/documentation/v5/docs/customization.md b/documentation/v5/docs/customization.md index c1840ae3..536d1b9c 100644 --- a/documentation/v5/docs/customization.md +++ b/documentation/v5/docs/customization.md @@ -228,7 +228,7 @@ function CustomNumeralNumericFormat(props) { Demo +
+ +### 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 +function CustomNegationNumberFormat({ prefix = '', suffix = '', ...restProps }) { + const [hasNegation, toggleNegation] = useState(''); + 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, + ...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 multiple characters are selected and user hits backspace, no need to handle anything manually + if (selectionStart !== selectionEnd) { + 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); + e.preventDefault(); + return; + } + + if (key === 'Backspace' && value[0] === '(' && selectionStart === props.prefix.length) { + toggleNegation(false); + e.preventDefault(); + return; + } + + onKeyDown(e); + }; + + return ; +} +``` + +
+ + Demo + +