diff --git a/README.md b/README.md index 3dc5d5df..69b39ed6 100755 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ In typescript you also have to enable `"esModuleInterop": true` in your tsconfig | removeFormatting | (formattedValue) => numericString | none | If you are providing custom format method and it add numbers as format you will need to add custom removeFormatting logic. [Demo](https://jinno.io/app/23/removeFormatting?source=react-number-format&displayType=input) | | mask | String (ex : \_) | `' '` | If mask defined, component will show non entered placed with masked value. [Demo](https://jinno.io/app/23/mask?source=react-number-format&displayType=input&format=###-####&mask=_) | | customInput | Component Reference | input | This allow supporting custom inputs with number format. | -| onValueChange | (values) => {} | none | onValueChange handler accepts [values object](#values-object). [Demo](https://jinno.io/app/23/onValueChange?source=react-number-format&displayType=input) | +| onValueChange | (values, sourceInfo) => {} | none | onValueChange handler accepts [values object](#values-object). [Demo](https://jinno.io/app/23/onValueChange?source=react-number-format&displayType=input) | | isAllowed | ([values](#values-object)) => true or false | none | A checker function to check if input value is valid or not. If this function returns false, the onChange method will not get triggered. [Demo](https://jinno.io/app/23/isAllowed?source=react-number-format&displayType=input) | | renderText | (formattedValue, customProps) => React Element | null | A renderText method useful if you want to render formattedValue in different element other than span. It also returns the custom props that are added to the component which can allow passing down props to the rendered element. [Demo](https://jinno.io/app/23/renderText?source=react-number-format&displayType=input) | | getInputRef | (elm) => void | null | Method to get reference of input, span (based on displayType prop) or the customInput's reference. See [Getting reference](#getting-reference). [Demo](https://jinno.io/app/23/getInputRef?source=react-number-format&displayType=input) | @@ -109,7 +109,7 @@ Its recommended to use formattedValue / value / floatValue based on the initial 6. 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 isNumericString prop to true. -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 no longer receives event object as second parameter. +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. ### Examples @@ -190,6 +190,21 @@ Output: ¥1,2345,6789 /> ``` +#### Accessing event and the source for onValueChangeTrigger + +```jsx + { + const { formattedValue, value } = values; + // Event is a Synthetic Event wrapper which holds target and other information. Source tells whether the reason for this function being triggered was an 'event' or due to a 'prop' change + const { event, source } = sourceInfo; + }} +/> +``` + #### Format with pattern : Format credit card in an input. [Demo](https://jinno.io/app/23/?source=react-number-format&value=4111111111111111&displayType=input&format=%23%23%23%23%20%23%23%23%23%20%23%23%23%23%20%23%23%23%23) ```jsx diff --git a/example/src/index.js b/example/src/index.js index a11eb702..7b0e281f 100644 --- a/example/src/index.js +++ b/example/src/index.js @@ -113,6 +113,18 @@ class App extends React.Component { /> +
+

Remove Trailing Zeros in Decimals (on blur)

+ +
+

Format with pattern : Format credit card in an input

@@ -152,6 +164,10 @@ class App extends React.Component {

Custom Numeral: add support for custom languages

+ + + + ); } diff --git a/package.json b/package.json index 5bbfb1f2..54217a7a 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": "4.7.3", + "version": "4.8.0", "main": "dist/react-number-format.cjs.js", "module": "dist/react-number-format.es.js", "author": "Sudhanshu Yadav", diff --git a/src/number_format.js b/src/number_format.js index 03ab78b3..99d69651 100644 --- a/src/number_format.js +++ b/src/number_format.js @@ -8,6 +8,7 @@ import { escapeRegExp, fixLeadingZero, limitToScale, + limitTrailingZeros, roundToPrecision, setCaretPosition, splitDecimal, @@ -25,6 +26,9 @@ const defaultProps = { decimalSeparator: '.', thousandsGroupStyle: 'thousand', fixedDecimalScale: false, + cutTrailingZeros: 0, + cutTrailingZerosOnBlur: false, + roundDecimalScale: true, prefix: '', suffix: '', allowNegative: true, @@ -128,7 +132,13 @@ class NumberFormat extends React.Component { //set state always when not in focus and formatted value is changed (focusedElm === null && formattedValue !== stateValue) ) { - this.updateValue({ formattedValue, numAsString, input: focusedElm }); + this.updateValue({ + formattedValue, + numAsString, + input: focusedElm, + source: 'prop', + event: null, + }); } } } @@ -472,6 +482,8 @@ class NumberFormat extends React.Component { suffix, allowNegative, thousandsGroupStyle, + cutTrailingZeros, + cutTrailingZerosOnBlur, } = this.props; const { thousandSeparator, decimalSeparator } = this.getSeparators(); @@ -494,6 +506,13 @@ class NumberFormat extends React.Component { //restore negation sign if (addNegation) beforeDecimal = '-' + beforeDecimal; + //cut trailing zeros + if(cutTrailingZeros){ + if((cutTrailingZerosOnBlur && this.focusedElm == null) || !cutTrailingZerosOnBlur){ + afterDecimal = limitTrailingZeros(afterDecimal, cutTrailingZeros, fixedDecimalScale) + } + } + numStr = beforeDecimal + ((hasDecimalSeparator && decimalSeparator) || '') + afterDecimal; return numStr; @@ -526,7 +545,7 @@ class NumberFormat extends React.Component { } formatValueProp(defaultValue: string | number) { - const { format, decimalScale, fixedDecimalScale, allowEmptyFormatting } = this.props; + const { format, decimalScale, fixedDecimalScale, allowEmptyFormatting, roundDecimalScale } = this.props; let { value, isNumericString } = this.props; // if value is undefined or null, use defaultValue instead @@ -553,7 +572,8 @@ class NumberFormat extends React.Component { //round the number based on decimalScale //format only if non formatted value is provided - if (isNumericString && !format && typeof decimalScale === 'number') { + //and if roundDecimalScale is set + if (roundDecimalScale && isNumericString && !format && typeof decimalScale === 'number') { value = roundToPrecision(value, decimalScale, fixedDecimalScale); } @@ -723,10 +743,12 @@ class NumberFormat extends React.Component { numAsString: string, inputValue: string, input: HTMLInputElement, + event: SyntheticInputEvent, + source: string, caretPos: number, setCaretPosition: Boolean, }) { - const { formattedValue, input, setCaretPosition = true } = params; + const { formattedValue, input, setCaretPosition = true, source, event } = params; let { numAsString, caretPos } = params; const { onValueChange } = this.props; const { value: lastValue } = this.state; @@ -772,7 +794,7 @@ class NumberFormat extends React.Component { this.setState({ value: formattedValue, numAsString }); // trigger onValueChange synchronously, so parent is updated along with the number format. Fix for #277, #287 - onValueChange(this.getValueObject(formattedValue, numAsString)); + onValueChange(this.getValueObject(formattedValue, numAsString), { event, source }); } } @@ -797,7 +819,14 @@ class NumberFormat extends React.Component { formattedValue = lastValue; } - this.updateValue({ formattedValue, numAsString, inputValue, input: el }); + this.updateValue({ + formattedValue, + numAsString, + inputValue, + input: el, + event: e, + source: 'event', + }); if (isChangeAllowed) { props.onChange(e); @@ -834,6 +863,8 @@ class NumberFormat extends React.Component { numAsString, input: e.target, setCaretPosition: false, + event: e, + source: 'event', }); onBlur(e); return; @@ -900,6 +931,8 @@ class NumberFormat extends React.Component { formattedValue: newValue, caretPos: newCaretPosition, input: el, + event: e, + source: 'event', }); } else if (!negativeRegex.test(value[expectedCaretPosition])) { while (!numRegex.test(value[newCaretPosition - 1]) && newCaretPosition > leftBound) { @@ -986,6 +1019,7 @@ class NumberFormat extends React.Component { thousandsGroupStyle, decimalScale, fixedDecimalScale, + roundDecimalScale, prefix, suffix, removeFormatting, @@ -1050,6 +1084,9 @@ if (process.env.NODE_ENV !== 'production') { thousandsGroupStyle: PropTypes.oneOf(['thousand', 'lakh', 'wan']), decimalScale: PropTypes.number, fixedDecimalScale: PropTypes.bool, + roundDecimalScale: PropTypes.bool, + cutTrailingZeros: PropTypes.number, + cutTrailingZerosOnBlur: PropTypes.bool, displayType: PropTypes.oneOf(['input', 'text']), prefix: PropTypes.string, suffix: PropTypes.string, diff --git a/src/utils.js b/src/utils.js index 432d81d8..8ddb1880 100644 --- a/src/utils.js +++ b/src/utils.js @@ -88,6 +88,31 @@ export function limitToScale(numStr: string, scale: number, fixedDecimalScale: b return str; } +/** + * limits decimal part such that it removes trailing zeros (if any) + * leaving only first *decimalsToLeaveIntact* intact + */ +export function limitTrailingZeros(afterDecimal: string, decimalsToLeaveIntact: number, fixedDecimalScale:boolean ){ + if(afterDecimal && afterDecimal.length > 0){ + let str = "0."+ afterDecimal; + let afterDecimalWithoutAnyTrailingZeros = str.replace(/^([\d,]+)$|^([\d,]+)\.0*$|^([\d,]+\.[0-9]*?)0*$/, "$1$2$3"); + let newAfterDecimal = afterDecimalWithoutAnyTrailingZeros.includes('.')? afterDecimalWithoutAnyTrailingZeros.split('.')[1]: '0'; + let result = '' + + if(newAfterDecimal.length < decimalsToLeaveIntact){ + const filler = fixedDecimalScale ? '0' : ''; + for (let i = 0; i <= decimalsToLeaveIntact - 1; i++) { + result += newAfterDecimal[i] || filler; + } + } + else{ + result = newAfterDecimal + } + return result + } + return afterDecimal; +} + function repeat(str, count) { return Array(count + 1).join(str); } diff --git a/test/library/input.spec.js b/test/library/input.spec.js index d9699e82..186bda9f 100644 --- a/test/library/input.spec.js +++ b/test/library/input.spec.js @@ -412,6 +412,25 @@ describe('NumberFormat as input', () => { }); }); + it('should call onValueChange with the right source information', () => { + const spy = jasmine.createSpy(); + const wrapper = shallow(); + + // Test prop change onValueChange + wrapper.setProps({ thousandSeparator: true }); + expect(spy.calls.argsFor(0)[1]).toEqual({ + event: null, + source: 'prop', + }); + + // Test with input change by simulateKeyInput + simulateKeyInput(wrapper.find('input'), '5', 0); + const { event, source } = spy.calls.argsFor(1)[1]; + const { key } = event; + expect(key).toEqual('5'); + expect(source).toEqual('event'); + }); + it('should treat Infinity value as empty string', () => { const wrapper = shallow(); expect(wrapper.state().value).toEqual(''); diff --git a/typings/number_format.d.ts b/typings/number_format.d.ts index b6ca4ab1..82fa38a4 100755 --- a/typings/number_format.d.ts +++ b/typings/number_format.d.ts @@ -16,6 +16,11 @@ declare module 'react-number-format' { value: string; } + export interface SourceInfo { + event: SyntheticInputEvent; + source: 'prop' | 'event'; + } + export type FormatInputValueFunction = (inputValue: string) => string; export interface SyntheticInputEvent extends React.SyntheticEvent { @@ -42,7 +47,7 @@ declare module 'react-number-format' { allowNegative?: boolean; allowEmptyFormatting?: boolean; allowLeadingZeros?: boolean; - onValueChange?: (values: NumberFormatValues) => void; + onValueChange?: (values: NumberFormatValues, sourceInfo: SourceInfo) => void; /** * these are already included in React.HTMLAttributes * onKeyDown: Function;