'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