12

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.

Cumulo Nimbus
  • 8,785
  • 9
  • 47
  • 68
  • Are you looking for validation or to restrict input? By means of validation you could add least inform the user that his input is invalid, which could give him more information than simply restricting him? I mean, in your last example I can still use the right mouseclick and past in a non-valid input :) – Icepickle Feb 09 '18 at 18:12
  • For something like "number of living humans in your house" I don't think it is necessary to specify "fractional or negative humans are not allowed". – Cumulo Nimbus Feb 09 '18 at 18:19

6 Answers6

4

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}
            />
        );
     }
}
Anthony
  • 6,422
  • 2
  • 17
  • 34
  • This is a good, simple way to achieve the desired end result, but it is my ultimate goal to not even allow the user to get to the point where they have invalid characters entered in the input – Cumulo Nimbus Feb 09 '18 at 18:20
  • this won't actually put any non-digits or '0' in via a keypress (can still copy paste text in though which is where a blur might help) – Anthony Feb 09 '18 at 18:35
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" />
Hamed
  • 1,351
  • 9
  • 23
1

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))
    }
}
Yurii
  • 126
  • 5
0

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()
                  }
                },
Jasurbek Nabijonov
  • 1,607
  • 3
  • 24
  • 37
0
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();
        }
      }}
    />;
  }
}
OneIvan
  • 11
  • 2
  • This answer is nice. It could be improved with an explanation of how this solves the problem / what was changed and why. – Corey Nov 04 '21 at 22:18
-1

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