Skip to content
This repository was archived by the owner on Jun 13, 2020. It is now read-only.

Commit c1b0521

Browse files
committed
Switch to decimal.js for number handling
1 parent 894faf3 commit c1b0521

File tree

5 files changed

+106
-71
lines changed

5 files changed

+106
-71
lines changed

package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"prepush": "npm run lint"
1313
},
1414
"dependencies": {
15+
"decimal.js": "^10.0.0",
1516
"prop-types": "^15.6.1"
1617
},
1718
"peerDependencies": {

rollup.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default {
88
file: 'lib/InputNumeric.js',
99
format: 'cjs'
1010
},
11-
external: ['react', 'prop-types'],
11+
external: ['react', 'prop-types', 'decimal.js'],
1212
plugins: [
1313
postcss(),
1414
babel({

src/InputNumeric.js

Lines changed: 94 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,139 @@
11
import React, { Component } from 'react';
22
import PropTypes from 'prop-types';
3+
import { Decimal } from 'decimal.js';
34

45

56
const propTypes = {
67
value: PropTypes.number.isRequired,
7-
step: PropTypes.number,
8-
min: PropTypes.number,
98
max: PropTypes.number,
9+
min: PropTypes.number,
1010
decimals: PropTypes.number,
11+
step: PropTypes.number,
12+
showTrailingZeros: PropTypes.bool,
13+
snapToStep: PropTypes.bool,
1114
onBlur: PropTypes.func,
1215
onChange: PropTypes.func
1316
};
1417

1518
const defaultProps = {
16-
step: 1,
17-
min: null,
1819
max: null,
19-
decimals: 0,
20+
min: null,
21+
decimals: 2,
22+
step: 1,
23+
showTrailingZeros: false,
24+
snapToStep: false,
2025
onBlur: null,
2126
onChange: null
2227
};
2328

2429
export default class InputNumeric extends Component {
30+
/**
31+
* Validate programmatically changed values
32+
*/
2533
static getDerivedStateFromProps(nextProps, prevState) {
26-
const newValue = nextProps.value;
27-
if (newValue !== prevState.value) {
34+
const newValue = new Decimal(nextProps.value);
35+
// Only perform validation if the entered value is different than the one stored in state
36+
if (!newValue.equals(prevState.value)) {
2837
return {
29-
newValue
38+
value: InputNumeric.validate(newValue, nextProps)
3039
};
3140
}else {
3241
return null;
3342
}
3443
}
3544

3645
/**
37-
* Round number to specified precision (and avoid the floating-number arithmetic issue)
38-
* Source: MDN
46+
* Value validation:
47+
* - Make sure it the number lies between props.min and props.max (if specified)
48+
* - Snap manually entered values to the nearest step (if specified)
3949
*/
40-
static round(number, decimals) {
41-
// Shift the decimals to get a whole number, then round using Math.round() and move the number
42-
// of whole numbers, based on the specified precision, back to decimals
43-
return this.shift(Math.round(this.shift(number, decimals, false)), decimals, true);
44-
}
50+
static validate(value, props) {
51+
let newValue = value;
52+
53+
// Make sure value is in specified range (between min and max)
54+
if (props.min !== null && newValue.lessThanOrEqualTo(props.min)) {
55+
// Value is min or smaller: set to min
56+
return new Decimal(props.min);
57+
} else if (props.max !== null && newValue.greaterThanOrEqualTo(props.max)) {
58+
// Value is max or larger: set to max
59+
return new Decimal(props.max);
60+
}
4561

46-
/**
47-
* Helper function for rounding: Move the number of decimals, based on the specified precision, to
48-
* a whole number
49-
* @param {boolean} reverseShift If true, round to one decimal place; if false, round to the tens
50-
* Source: MDN
51-
*/
52-
static shift(number, decimals, reverseShift) {
53-
let precision = decimals;
54-
if (reverseShift) {
55-
precision = -precision;
62+
// Snap to step if option is enabled
63+
if (props.snapToStep === true) {
64+
newValue = newValue.toNearest(props.step);
5665
}
57-
const numArray = (`${number}`).split('e');
58-
return +(`${numArray[0]}e${numArray[1] ? (+numArray[1] + precision) : precision}`);
66+
67+
return newValue;
5968
}
6069

6170
constructor(props) {
6271
super(props);
63-
this.mouseDownDelay = 250;
64-
this.mouseDownInterval = 75;
72+
this.mouseDownDelay = 250; // duration until mouseDown is treated as such (instead of click)
73+
this.mouseDownInterval = 75; // interval for increasing/decreasing value on mouseDown
6574

6675
this.state = {
67-
newValue: this.props.value
76+
value: new Decimal(this.props.value), // final, validated number
77+
valueEntered: '' // temporary, unvalidated number displayed while user is editing the input
6878
};
6979
}
7080

71-
onChange(e) {
81+
onInputChange(e) {
82+
// Temporarily save the new value entered in the input field
7283
this.setState({
73-
newValue: e.target.value
84+
valueEntered: e.target.value
7485
});
7586
}
7687

77-
onBlur() {
78-
const newValue = this.validate(this.state.newValue);
88+
onInputBlur() {
89+
// Validate and save this.state.valueEntered, execute this.props.onChange and this.props.onBlur
90+
const value = InputNumeric.validate(new Decimal(this.state.valueEntered), this.props);
91+
this.setState({
92+
value,
93+
valueEntered: ''
94+
});
7995
if (this.props.onChange) {
80-
this.props.onChange(newValue);
96+
this.props.onChange(value.toNumber());
8197
}
8298
if (this.props.onBlur) {
83-
this.props.onBlur(newValue);
84-
}
85-
}
86-
87-
validate(newValue) {
88-
if (newValue === '') {
89-
// no input: use previous value
90-
return this.state.newValue;
91-
}
92-
const newNumber = parseFloat(newValue);
93-
if (this.props.min && newNumber <= this.props.min) {
94-
// value is min or smaller: set to min
95-
return this.props.min;
96-
} else if (this.props.max && newNumber >= this.props.max) {
97-
// value is max or larger: set to max
98-
return this.props.max;
99-
} else {
100-
// value is between min and max: round number to specified number of decimals
101-
return InputNumeric.round(newNumber, this.props.decimals);
99+
this.props.onBlur(value.toNumber());
102100
}
103101
}
104102

105103
/**
106104
* Decrement the input field's value by one step (this.props.step)
107105
*/
108106
decrement() {
109-
const newValue = this.validate(this.state.newValue - this.props.step);
110-
this.setState({
111-
newValue
112-
});
107+
const oldValue = this.state.value;
108+
if (oldValue.modulo(this.props.step).isZero()) {
109+
// If current value is divisible by step: Subtract step
110+
this.setState({
111+
value: InputNumeric.validate(oldValue.minus(this.props.step), this.props)
112+
});
113+
} else {
114+
// If current value is not divisible by step: Round to nearest lower multiple of step
115+
this.setState({
116+
value: oldValue.toNearest(this.props.step, Decimal.ROUND_DOWN)
117+
});
118+
}
113119
}
114120

115121
/**
116122
* Increment the input field's value by one step (this.props.step)
117123
*/
118124
increment() {
119-
const newValue = this.validate(this.state.newValue + this.props.step);
120-
this.setState({
121-
newValue
122-
});
125+
const oldValue = this.state.value;
126+
if (oldValue.modulo(this.props.step).isZero()) {
127+
// If current value is divisible by step: Add step
128+
this.setState({
129+
value: InputNumeric.validate(oldValue.plus(this.props.step), this.props)
130+
});
131+
} else {
132+
// If current value is not divisible by step: Round to nearest higher multiple of step
133+
this.setState({
134+
value: oldValue.toNearest(this.props.step, Decimal.ROUND_UP)
135+
});
136+
}
123137
}
124138

125139
/**
@@ -157,14 +171,27 @@ export default class InputNumeric extends Component {
157171
clearInterval(this.interval);
158172
}
159173
if (this.props.onChange) {
160-
this.props.onChange(this.state.newValue);
174+
this.props.onChange(this.state.value.toNumber());
161175
}
162176
if (this.props.onBlur) {
163-
this.props.onBlur(this.state.newValue);
177+
this.props.onBlur(this.state.value.toNumber());
164178
}
165179
}
166180

167181
render() {
182+
// Determine value to be displayed in the input field
183+
let displayedValue;
184+
if (this.state.valueEntered !== '') {
185+
// Display entered (non-validated) value while input field is being edited
186+
displayedValue = this.state.valueEntered;
187+
} else if (this.props.showTrailingZeros === true) {
188+
// Add trailing zeros if option is enabled
189+
displayedValue = this.state.value.toFixed(this.props.decimals);
190+
} else {
191+
// Round to specified number of decimals
192+
displayedValue = this.state.value.toDecimalPlaces(this.props.decimals);
193+
}
194+
168195
return (
169196
<div className="number-input">
170197
<button
@@ -177,10 +204,9 @@ export default class InputNumeric extends Component {
177204
</button>
178205
<input
179206
type="number"
180-
value={this.state.newValue}
181-
step={this.props.step}
182-
onChange={e => this.onChange(e)}
183-
onBlur={() => this.onBlur()}
207+
value={displayedValue}
208+
onChange={e => this.onInputChange(e)}
209+
onBlur={() => this.onInputBlur()}
184210
/>
185211
<button
186212
type="button"

stories/InputContainer.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ export default class InputContainer extends Component {
1515
<InputNumeric
1616
value={this.state.value}
1717
onChange={value => this.setState({ value })}
18-
min={0}
19-
max={50}
18+
min={-10}
19+
max={10}
20+
decimals={4}
21+
step={0.2}
22+
snapToStep
2023
/>
2124
);
2225
}

0 commit comments

Comments
 (0)