'React: Number input with no negative, decimal, or zero value values
Consider an input of type number
, I would like this number input to only allow a user to enter one positive, non-zero, integer (no decimals) number. A simple implementation using min
and step
looks like this:
class PositiveIntegerInput extends React.Component {
render () {
return <input type='number' min='1' step='1'></input>
}
}
ReactDOM.render(
<PositiveIntegerInput />,
document.getElementById('container')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<p>
Try to input a decimal or negative number or zero:
</p>
<div id="container"></div>
The above code works fine if a user sticks to ONLY clicking the up/down arrows in the number input, but as soon a the user starts using the keyboard they will have no problem entering numbers like -42
, 3.14
and 0
Ok, lets try adding some onKeyDown
handling to disallow this loophole:
class PositiveIntegerInput extends React.Component {
constructor (props) {
super(props)
this.handleKeypress = this.handleKeypress.bind(this)
}
handleKeypress (e) {
const characterCode = e.key
if (characterCode === 'Backspace') return
const characterNumber = Number(characterCode)
if (characterNumber >= 0 && characterNumber <= 9) {
if (e.currentTarget.value && e.currentTarget.value.length) {
return
} else if (characterNumber === 0) {
e.preventDefault()
}
} else {
e.preventDefault()
}
}
render () {
return (
<input type='number' onKeyDown={this.handleKeypress} min='1' step='1'></input>
)
}
}
ReactDOM.render(
<PositiveIntegerInput />,
document.getElementById('container')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<p>
Try to input a decimal or negative number or zero:
</p>
<div id="container"></div>
Now everything almost appears to work as desired. However if a user highlights all the digits in the text input and then types over this selection with a 0
the input will allow 0
to be entered as a value.
To fix this issue I added an onBlur
function that checks if the input value is 0
and if so changes it to a 1
:
class PositiveIntegerInput extends React.Component {
constructor (props) {
super(props)
this.handleKeypress = this.handleKeypress.bind(this)
this.handleBlur = this.handleBlur.bind(this)
}
handleBlur (e) {
if (e.currentTarget.value === '0') e.currentTarget.value = '1'
}
handleKeypress (e) {
const characterCode = e.key
if (characterCode === 'Backspace') return
const characterNumber = Number(characterCode)
if (characterNumber >= 0 && characterNumber <= 9) {
if (e.currentTarget.value && e.currentTarget.value.length) {
return
} else if (characterNumber === 0) {
e.preventDefault()
}
} else {
e.preventDefault()
}
}
render () {
return (
<input
type='number'
onKeyDown={this.handleKeypress}
onBlur={this.handleBlur}
min='1'
step='1'
></input>
)
}
}
ReactDOM.render(
<PositiveIntegerInput />,
document.getElementById('container')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<p>
Try to input a decimal or negative number or zero:
</p>
<div id="container"></div>
Is there a better way to implement a number input with this type of criteria? It seems pretty crazy to write all this overhead for an input to allow only positive, non-zero integers... there must be a better way.
Solution 1:[1]
If you did it as a controlled input with the value in component state, you could prevent updating state onChange if it didn't meet your criteria. e.g.
class PositiveInput extends React.Component {
state = {
value: ''
}
onChange = e => {
//replace non-digits with blank
const value = e.target.value.replace(/[^\d]/,'');
if(parseInt(value) !== 0) {
this.setState({ value });
}
}
render() {
return (
<input
type="text"
value={this.state.value}
onChange={this.onChange}
/>
);
}
}
Solution 2:[2]
Here's a number spinner implantation in React Bootstrap. It only accepts positive integers and you can set min, max and default values.
class NumberSpinner extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
oldVal: 0,
value: 0,
maxVal: 0,
minVal: 0
};
this.handleIncrease = this.handleIncrease.bind(this);
this.handleDecrease = this.handleDecrease.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleBlur = this.handleBlur.bind(this);
}
componentDidMount() {
this.setState({
value: this.props.value,
minVal: this.props.min,
maxVal: this.props.max
});
}
handleBlur() {
const blurVal = parseInt(this.state.value, 10);
if (isNaN(blurVal) || blurVal > this.state.maxVal || blurVal < this.state.minVal) {
this.setState({
value: this.state.oldVal
});
this.props.changeVal(this.state.oldVal, this.props.field);
}
}
handleChange(e) {
const re = /^[0-9\b]+$/;
if (e.target.value === '' || re.test(e.target.value)) {
const blurVal = parseInt(this.state.value, 10);
if (blurVal <= this.state.maxVal && blurVal >= this.state.minVal) {
this.setState({
value: e.target.value,
oldVal: this.state.value
});
this.props.changeVal(e.target.value, this.props.field);
} else {
this.setState({
value: this.state.oldVal
});
}
}
}
handleIncrease() {
const newVal = parseInt(this.state.value, 10) + 1;
if (newVal <= this.state.maxVal) {
this.setState({
value: newVal,
oldVal: this.state.value
});
this.props.changeVal(newVal, this.props.field);
};
}
handleDecrease() {
const newVal = parseInt(this.state.value, 10) - 1;
if (newVal >= this.state.minVal) {
this.setState({
value: newVal,
oldVal: this.state.value
});
this.props.changeVal(newVal, this.props.field);
};
}
render() {
return ( <
ReactBootstrap.ButtonGroup size = "sm"
aria-label = "number spinner"
className = "number-spinner" >
<
ReactBootstrap.Button variant = "secondary"
onClick = {
this.handleDecrease
} > - < /ReactBootstrap.Button> <
input value = {
this.state.value
}
onChange = {
this.handleChange
}
onBlur = {
this.handleBlur
}
/> <
ReactBootstrap.Button variant = "secondary"
onClick = {
this.handleIncrease
} > + < /ReactBootstrap.Button> < /
ReactBootstrap.ButtonGroup >
);
}
}
class App extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
value1: 1,
value2: 12
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(value, field) {
this.setState({ [field]: value });
}
render() {
return (
<div>
<div>Accept numbers from 1 to 10 only</div>
< NumberSpinner changeVal = {
() => this.handleChange
}
value = {
this.state.value1
}
min = {
1
}
max = {
10
}
field = 'value1'
/ >
<br /><br />
<div>Accept numbers from 10 to 20 only</div>
< NumberSpinner changeVal = {
() => this.handleChange
}
value = {
this.state.value2
}
min = {
10
}
max = {
20
}
field = 'value2'
/ >
<br /><br />
<div>If the number is out of range, the blur event will replace it with the last valid number</div>
</div>);
}
}
ReactDOM.render( < App / > ,
document.getElementById('root')
);
.number-spinner {
margin: 2px;
}
.number-spinner input {
width: 30px;
text-align: center;
}
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/react-bootstrap@next/dist/react-bootstrap.min.js" crossorigin></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css" crossorigin="anonymous">
<div id="root" />
Solution 3:[3]
That's how number input works. To simplify the code you could try to use validity state (if your target browsers support it)
onChange(e) {
if (!e.target.validity.badInput) {
this.setState(Number(e.target.value))
}
}
Solution 4:[4]
I had a similar problem when I need to allow only positive number, fount solution on another question on StackOverflow(https://stackoverflow.com/a/34783480/5646315).
Example code that I implemented for react-final-form
.
P.S: it is not the most elegant solution.
onKeyDown: (e: React.KeyboardEvent) => {
if (!((e.keyCode > 95 && e.keyCode < 106) || (e.keyCode > 47 && e.keyCode < 58) || e.keyCode === 8)) {
e.preventDefault()
}
},
Solution 5:[5]
class BasketItem extends React.Component {
constructor(props) {
super(props);
this.state = {
countBasketItem: props.qnt,
};
}
componentDidMount() {
const $ = window.$;
// using jquery-styler-form(bad practice)
$('input[type="number"]').styler();
// minus 1
$(`#basket_${this.props.id} .jq-number__spin.minus`).click(() => {
if (this.state.countBasketItem > 1) {
this.setState({ countBasketItem: +this.state.countBasketItem - 1 });
this.setCountProduct();
}
});
// plus 1
$(`#basket_${this.props.id} .jq-number__spin.plus`).click(() => {
this.setState({ countBasketItem: +this.state.countBasketItem + 1 });
this.setCountProduct();
});
}
onChangeCount = (e) => {
let countBasketItem = +e.target.value
countBasketItem = (countBasketItem === 0) ? '' : (countBasketItem > 999) ? 999 : countBasketItem;
this.setState({ countBasketItem })
};
onBlurCount() {
// number empty
if (+this.state.countBasketItem == 0 || isNaN(+this.state.countBasketItem)) {
this.setState({ countBasketItem: 1 });
}
this.setCountProduct();
}
setCountProduct = (colrKey = this.props.colr.key, idProduct = this.props.product.id, qnt) => {
qnt = +this.state.countBasketItem || 1; // if don't work setState
this.props.basket.editCountProduct(idProduct, colrKey, qnt); // request on server
};
render() {
return;
<input
type="number"
className="number"
min="1"
value={this.state.countBasketItem}
onChange={this.onChangeCount.bind(this)}
// onFocused
onBlur={this.onBlurCount.bind(this)}
// input only numbers
onKeyPress={(event) => {
if (!/[0-9]/.test(event.key)) {
event.preventDefault();
}
}}
/>;
}
}
Solution 6:[6]
This is not a react problem, but a html problem as you can see over here https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number and I have made a stateless example you can see right here https://codesandbox.io/s/l5k250m87
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | Hamed |
Solution 3 | Yurii |
Solution 4 | Jasurbek Nabijonov |
Solution 5 | |
Solution 6 |