Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize render #756

Merged
merged 10 commits into from
May 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions documentation/v5/docs/numeric_format.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,61 @@ import { NumericFormat } from 'react-number-format';
- [See Common Props](/docs/props)

**Other than this it accepts all the props which can be given to a input or span based on displayType you selected.**

## Other exports

With v5.0 we expose some more utils/hooks which can be used for customization or other utilities

### numericFormatter `(numString: string, props: NumericFormatProps) => string`

In some places we need to just format the number before we pass it down as value, or in general just to render it. In such cases `numericFormatter` can be used directly.

**Parameters**

1st. `numString`(non formatted number string)

2nd. `props` (the format props applicable on numeric format)

**Return**
`formattedString` returns the formatted number.

### removeNumericFormat `(inputValue: string, changeMeta: ChangeMeta, props: NumericFormatProps) => string`

Most of the time you might not need this, but in some customization case you might wan't to write a patched version on top of removeNumericFormat.

However for customization case its recommended to use `useNumericFormat` and patch the methods it returns, as lot of other handling is done in the hook.

**Parameters**

1st. `inputValue`: the value after user has typed, this will be formatted value with the additional character typed by user.

2nd. `changeMeta`: This is the change information rnf sends internally, its basically the change information from the last formatted value and the current typed input value.

The type is following

```js
{
from: {start: number, end: number},
to: {start: number, end: number},
lastValue: string
}
```

3rd. `props`: all the numeric format props

**Return**
`numString` returns the number in string format.

### getNumericCaretBoundary `(formattedValue: string, props: NumericFormatProps) => Array<boolean>`

This method returns information about what all position in formatted value where caret can be places, it returns n+1 length array of booleans(where n is the length of formattedValue).

Most of time you don't need this, but in case if you very specific usecase you can patch the function to handle your case.

See more details on [Concept](https://s-yadav.github.io/react-number-format/docs/customization/#concept)

### useNumericFormat: `(props: NumericFormatProps) => NumberFormatBaseProps`

The whole numeric format logic is inside useNumericFormat hook, this returns all the required props which can be passed to `NumberFormatBase`. For customization you can use to patch methods returned by `useNumericFormat` and pass to `NumberFormatBase`.

See more details in [Customization](https://s-yadav.github.io/react-number-format/docs/customization/)
58 changes: 58 additions & 0 deletions documentation/v5/docs/pattern_format.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,61 @@ Demo
- [See Common Props](/docs/props)

**Other than this it accepts all the props which can be given to a input or span based on displayType you selected.**

## Other exports

With v5.0 we expose some more utils/hooks which can be used for customization or other utilities

### patternFormatter `(numString: string, props: PatternFormatProps) => string`

In some places we need to just format the number before we pass it down as value, or in general just to render it. In such cases `patternFormatter` can be used directly.

**Parameters**

1st. `numString`(non formatted number string)

2nd. `props` (the format props applicable on numeric format)

**Return**
`formattedString` returns the formatted number.

### removePatternFormat `(inputValue: string, changeMeta: ChangeMeta, props: PatternFormatProps) => string`

Most of the time you might not need this, but in some customization case you might wan't to write a patched version on top of removePatternFormat.

However for customization case its recommended to use `usePatternFormat` and patch the methods it returns, as lot of other handling is done in the hook.

**Parameters**

1st. `inputValue`: the value after user has typed, this will be formatted value with the additional character typed by user.

2nd. `changeMeta`: This is the change information rnf sends internally, its basically the change information from the last formatted value and the current typed input value.

The type is following

```js
{
from: {start: number, end: number},
to: {start: number, end: number},
lastValue: string
}
```

3rd. `props`: all the numeric format props

**Return**
`numString` returns the number in string format.

### getPatternCaretBoundary `(formattedValue: string, props: PatternFormatProps) => Array<boolean>`

This method returns information about what all position in formatted value where caret can be places, it returns n+1 length array of booleans(where n is the length of formattedValue).

Most of time you don't need this, but in case if you very specific usecase you can patch the function to handle your case.

See more details on [Concept](https://s-yadav.github.io/react-number-format/docs/customization/#concept)

### usePatternFormat: `(props: PatternFormatProps) => NumberFormatBaseProps`

The whole numeric format logic is inside usePatternFormat hook, this returns all the required props which can be passed to `NumberFormatBase`. For customization you can use to patch methods returned by `usePatternFormat` and pass to `NumberFormatBase`.

See more details in [Customization](https://s-yadav.github.io/react-number-format/docs/customization/)
16 changes: 6 additions & 10 deletions documentation/v5/docs/props.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,18 +160,14 @@ const MAX_LIMIT = 1000;

**default**: false

If value is passed as string representation of numbers (unformatted) then this should be passed as `true`.
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`.

**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).

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

<NumericFormat
type="text"
value="123456789"
valueIsNumericString={true}
decimalSeparator=","
displayType="input"
/>;
<PatternFormat format="+1 (###) ###-####" value="123456789" valueIsNumericString={true} />;
```

<details>
Expand All @@ -194,7 +190,7 @@ import { NumericFormat } 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 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 as number on it. See [valueIsNumericString](#valueisnumericstring-boolean) for more details.
:::

```js
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "react-number-format",
"description": "React component to format number in an input or as a text.",
"version": "5.1.4",
"version": "5.2.0",
"main": "dist/react-number-format.cjs.js",
"module": "dist/react-number-format.es.js",
"types": "types/index.d.ts",
Expand Down
88 changes: 60 additions & 28 deletions src/number_format_base.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState, useRef } from 'react';
import React, { useEffect, useState, useRef, useLayoutEffect } from 'react';
import {
FormatInputValueFunction,
NumberFormatBaseProps,
Expand Down Expand Up @@ -62,35 +62,13 @@ export default function NumberFormatBase<BaseType = InputAttributes>(
onValueChange,
);

const lastUpdatedValue = useRef<string>();
const lastUpdatedValue = useRef({ formattedValue, numAsString });

const _onValueChange: NumberFormatBaseProps['onValueChange'] = (values, source) => {
lastUpdatedValue.current = values.formattedValue;
lastUpdatedValue.current = { formattedValue: values.formattedValue, numAsString: values.value };
onFormattedValueChange(values, source);
};

// check if there is any change in the value due to props change
useEffect(() => {
const newFormattedValue = (format as FormatInputValueFunction)(numAsString);

// if the formatted value is not synced to parent, or if the formatted value is different
if (lastUpdatedValue.current === undefined || newFormattedValue !== lastUpdatedValue.current) {
const input = focusedElm.current;

// formatting can remove some of the number chars, so we need to fine number string again
const _numAsString = removeFormatting(newFormattedValue, undefined);

updateValue({
formattedValue: newFormattedValue,
numAsString: _numAsString,
input,
setCaretPosition: true,
source: SourceType.props,
event: undefined,
});
}
});

const [mounted, setMounted] = useState(false);
const focusedElm = useRef<HTMLInputElement | null>(null);

Expand Down Expand Up @@ -128,12 +106,18 @@ export default function NumberFormatBase<BaseType = InputAttributes>(
caretPos: number,
currentValue: string,
) => {
// don't reset the caret position when the whole input content is selected
if (el.selectionStart === 0 && el.selectionEnd === el.value.length) return;

/* setting caret position within timeout of 0ms is required for mobile chrome,
otherwise browser resets the caret position after we set it
We are also setting it without timeout so that in normal browser we don't see the flickering */
setCaretPosition(el, caretPos);

timeout.current.setCaretTimeout = setTimeout(() => {
if (el.value === currentValue) setCaretPosition(el, caretPos);
if (el.value === currentValue && el.selectionStart !== el.selectionEnd) {
setCaretPosition(el, caretPos);
}
}, 0);
};

Expand All @@ -159,7 +143,7 @@ export default function NumberFormatBase<BaseType = InputAttributes>(
return updatedCaretPos;
};

const updateValue = (params: {
const updateValueAndCaretPosition = (params: {
formattedValue?: string;
numAsString: string;
inputValue?: string;
Expand Down Expand Up @@ -219,6 +203,49 @@ export default function NumberFormatBase<BaseType = InputAttributes>(
}
};

/**
* if the formatted value is not synced to parent, or if the formatted value is different from last synced value sync it
* we also don't need to sync to the parent if no formatting is applied
* if the formatting props is removed, in which case last formatted value will be different from the numeric string value
* in such case we need to inform the parent.
*/
useEffect(() => {
const { formattedValue: lastFormattedValue, numAsString: lastNumAsString } =
lastUpdatedValue.current;
if (
formattedValue !== lastFormattedValue &&
(formattedValue !== numAsString || lastFormattedValue !== lastNumAsString)
) {
_onValueChange(getValueObject(formattedValue, numAsString), {
event: undefined,
source: SourceType.props,
});
}
}, [formattedValue, numAsString]);

// also if formatted value is changed from the props, we need to update the caret position
// keep the last caret position if element is focused
const currentCaretPosition = focusedElm.current
? geInputCaretPosition(focusedElm.current)
: undefined;

useLayoutEffect(() => {
const input = focusedElm.current;
if (formattedValue !== lastUpdatedValue.current.formattedValue && input) {
const caretPos = getNewCaretPosition(
lastUpdatedValue.current.formattedValue,
formattedValue,
currentCaretPosition,
);
/**
* set the value imperatively, as we set the caret position as well imperatively.
* This is to keep value and caret position in sync
*/
input.value = formattedValue;
setPatchedCaretPosition(input, caretPos, formattedValue);
}
}, [formattedValue]);

const formatInputValue = (
inputValue: string,
event:
Expand All @@ -244,11 +271,12 @@ export default function NumberFormatBase<BaseType = InputAttributes>(
const currentCaretPosition = geInputCaretPosition(input);

const caretPos = getNewCaretPosition(inputValue, formattedValue, currentCaretPosition);
input.value = formattedValue;
setPatchedCaretPosition(input, caretPos, formattedValue);
return false;
}

updateValue({
updateValueAndCaretPosition({
formattedValue: _formattedValue,
numAsString: _numAsString,
inputValue,
Expand Down Expand Up @@ -298,6 +326,10 @@ export default function NumberFormatBase<BaseType = InputAttributes>(
if (key === 'ArrowLeft' || key === 'ArrowRight') {
const direction = key === 'ArrowLeft' ? 'left' : 'right';
newCaretPosition = correctCaretPosition(value, expectedCaretPosition, direction);
// arrow left or right only moves the caret, so no need to handle the event, if we are handling it manually
if (newCaretPosition !== expectedCaretPosition) {
e.preventDefault();
}
} else if (key === 'Delete' && !isValidInputCharacter(value[expectedCaretPosition])) {
// in case of delete go to closest caret boundary on the right side
newCaretPosition = correctCaretPosition(value, expectedCaretPosition, 'right');
Expand Down
26 changes: 21 additions & 5 deletions src/numeric_format.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import {
useInternalValues,
isNil,
roundToPrecision,
isNanValue,
setCaretPosition,
toNumericString,
charIsNumber,
isNotValidValue,
} from './utils';
import {
NumericFormatProps,
Expand Down Expand Up @@ -118,6 +118,19 @@ function getNumberRegex(decimalSeparator: string, global: boolean) {
return new RegExp(`(^-)|[0-9]|${escapeRegExp(decimalSeparator)}`, global ? 'g' : undefined);
}

function isNumericString(
val: string | number | undefined | null,
prefix?: string,
suffix?: string,
) {
// for empty value we can always treat it as numeric string
if (val === '') return true;

return (
!prefix?.match(/\d/) && !suffix?.match(/\d/) && typeof val === 'string' && !isNaN(Number(val))
);
}

export function removeFormatting<BaseType = InputAttributes>(
value: string,
changeMeta: ChangeMeta = getDefaultChangeMeta(value),
Expand Down Expand Up @@ -347,16 +360,19 @@ export function useNumericFormat<BaseType = InputAttributes>(
const _removeFormatting: RemoveFormattingFunction = (inputValue, changeMeta) =>
removeFormatting(inputValue, changeMeta, props);

let _valueIsNumericString = valueIsNumericString;
const _value = isNil(value) ? defaultValue : value;

// try to figure out isValueNumericString based on format prop and value
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) => {
if (isNil(value) || isNanValue(value)) return value;
if (isNotValidValue(value)) return value;

if (typeof value === 'number') {
value = toNumericString(value);
Expand Down
Loading