Skip to content

Commit

Permalink
- Fix #774
Browse files Browse the repository at this point in the history
- Fix custom numeral example
- Added support to inform rnf to treat two character as same.
  • Loading branch information
s-yadav committed Aug 13, 2023
1 parent 1d1171f commit e502b2c
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 29 deletions.
48 changes: 41 additions & 7 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 Down
51 changes: 48 additions & 3 deletions example/src/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<NumberFormatBase
format={_format}
removeFormatting={_removeFormatting}
isCharacterSame={_isCharacterSame}
{...rest}
/>
);
}

class App extends React.Component {
constructor() {
super();
Expand Down Expand Up @@ -84,7 +123,8 @@ class App extends React.Component {
<h3>Custom thousand separator : Format currency in input</h3>
<div>ThousandSeparator: '.', decimalSeparator=','</div>
<div>
<NumericFormat thousandSeparator="." decimalSeparator="," prefix="$" />
<NumericFormat value={1234567.8901} thousandSeparator="." decimalSeparator="," />,
<NumericFormat thousandSeparator="." decimalSeparator="," prefix="$" suffix=" /-" />
</div>
<br />
<div>ThousandSeparator: ' ', decimalSeparator='.'</div>
Expand Down Expand Up @@ -152,7 +192,12 @@ class App extends React.Component {

<div className="example">
<h3>Custom Numeral: add support for custom languages </h3>
<NumericFormat customNumerals={['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']} />
<CustomNumeralNumericFormat
prefix="$"
decimalSeparator=","
suffix="/-"
allowedDecimalSeparators={[',', '.']}
/>
</div>
</div>
);
Expand Down
4 changes: 3 additions & 1 deletion src/number_format_base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default function NumberFormatBase<BaseType = InputAttributes>(
value: propValue,
getCaretBoundary = caretUnknownFormatBoundary,
isValidInputCharacter = charIsNumber,
isCharacterSame,
...otherProps
} = props;

Expand Down Expand Up @@ -135,6 +136,7 @@ export default function NumberFormatBase<BaseType = InputAttributes>(
caretPos,
caretBoundary,
isValidInputCharacter,
isCharacterSame,
);

//correct caret position if its outside of editable area
Expand Down Expand Up @@ -230,7 +232,7 @@ export default function NumberFormatBase<BaseType = InputAttributes>(
: 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;
Expand Down
42 changes: 39 additions & 3 deletions src/numeric_format.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
toNumericString,
charIsNumber,
isNotValidValue,
findChangeRange,
} from './utils';
import {
NumericFormatProps,
Expand All @@ -23,6 +24,7 @@ import {
FormatInputValueFunction,
RemoveFormattingFunction,
NumberFormatBaseProps,
IsCharacterSame,
} from './types';
import NumberFormatBase from './number_format_base';

Expand Down Expand Up @@ -334,9 +336,9 @@ export function useNumericFormat<BaseType = InputAttributes>(
props = validateAndUpdateProps(props);

const {
decimalSeparator = '.',
/* eslint-disable no-unused-vars */
allowedDecimalSeparators,
decimalSeparator: _decimalSeparator,
allowedDecimalSeparators: _allowedDecimalSeparators,
thousandsGroupStyle,
suffix,
allowNegative,
Expand All @@ -355,6 +357,9 @@ export function useNumericFormat<BaseType = InputAttributes>(
...restProps
} = props;

// get derived decimalSeparator and allowedDecimalSeparators
const { decimalSeparator, allowedDecimalSeparators } = getSeparators(props);

const _format: FormatInputValueFunction = (numStr) => format(numStr, props);

const _removeFormatting: RemoveFormattingFunction = (inputValue, changeMeta) =>
Expand Down Expand Up @@ -421,7 +426,6 @@ export function useNumericFormat<BaseType = InputAttributes>(
}

// 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 &&
Expand Down Expand Up @@ -492,11 +496,43 @@ export function useNumericFormat<BaseType = InputAttributes>(
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<BaseType>),
value: formattedValue,
valueIsNumericString: false,
isValidInputCharacter,
isCharacterSame,
onValueChange: _onValueChange,
format: _format,
removeFormatting: _removeFormatting,
Expand Down
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ type NumberFormatProps<Props, BaseType = InputAttributes> = 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';
Expand All @@ -77,6 +85,7 @@ type NumberFormatBase = {
onBlur?: InputAttributes['onBlur'];
getCaretBoundary?: (formattedValue: string) => boolean[];
isValidInputCharacter?: (character: string) => boolean;
isCharacterSame?: IsCharacterSame;
};

export type NumberFormatBaseProps<BaseType = InputAttributes> = NumberFormatProps<
Expand Down
Loading

0 comments on commit e502b2c

Please sign in to comment.