javascriptjqueryhtml

HTML text input allow only numeric input


Is there a quick way to set an HTML text input (<input type=text />) to only allow numeric keystrokes (plus '.')?


Solution

  • JavaScript

    Update: easier solution seems to use beforeinput event.

    You can filter the input values of a text <input> with the following setInputFilter function (supports Copy+Paste, Drag+Drop, keyboard shortcuts, context menu operations, non-typeable keys, the caret position, different keyboard layouts, validity error message, and all browsers since IE 9):

    // Restricts input for the given textbox to the given inputFilter function.
    function setInputFilter(textbox, inputFilter, errMsg) {
      [ "input", "keydown", "keyup", "mousedown", "mouseup", "select", "contextmenu", "drop", "focusout" ].forEach(function(event) {
        textbox.addEventListener(event, function(e) {
          if (inputFilter(this.value)) {
            // Accepted value.
            if ([ "keydown", "mousedown", "focusout" ].indexOf(e.type) >= 0){
              this.classList.remove("input-error");
              this.setCustomValidity("");
            }
    
            this.oldValue = this.value;
            this.oldSelectionStart = this.selectionStart;
            this.oldSelectionEnd = this.selectionEnd;
          }
          else if (this.hasOwnProperty("oldValue")) {
            // Rejected value: restore the previous one.
            this.classList.add("input-error");
            this.setCustomValidity(errMsg);
            this.reportValidity();
            this.value = this.oldValue;
            this.setSelectionRange(this.oldSelectionStart, this.oldSelectionEnd);
          }
          else {
            // Rejected value: nothing to restore.
            this.value = "";
          }
        });
      });
    }
    

    You can now use the setInputFilter function to install an input filter:

    setInputFilter(document.getElementById("myTextBox"), function(value) {
      return /^\d*\.?\d*$/.test(value); // Allow digits and '.' only, using a RegExp.
    }, "Only digits and '.' are allowed");
    

    Apply your preferred style to the input-error class. Here’s a suggestion:

    .input-error{
      outline: 1px solid red;
    }
    

    Note that you still must do server side validation!

    Another caveat is that this will break the undo stack since it sets this.value directly. This means that CtrlZ will not work to undo inputs after typing an invalid character.

    Demo

    See the JSFiddle demo for more input filter examples or run the Stack snippet below:

    // Restricts input for the given textbox to the given inputFilter.
    function setInputFilter(textbox, inputFilter, errMsg) {
      [ "input", "keydown", "keyup", "mousedown", "mouseup", "select", "contextmenu", "drop", "focusout" ].forEach(function(event) {
        textbox.addEventListener(event, function(e) {
          if (inputFilter(this.value)) {
            // Accepted value.
            if ([ "keydown", "mousedown", "focusout" ].indexOf(e.type) >= 0) {
              this.classList.remove("input-error");
              this.setCustomValidity("");
            }
            
            this.oldValue = this.value;
            this.oldSelectionStart = this.selectionStart;
            this.oldSelectionEnd = this.selectionEnd;
          }
          else if (this.hasOwnProperty("oldValue")) {
            // Rejected value: restore the previous one.
            this.classList.add("input-error");
            this.setCustomValidity(errMsg);
            this.reportValidity();
            this.value = this.oldValue;
            this.setSelectionRange(this.oldSelectionStart, this.oldSelectionEnd);
          }
          else {
            // Rejected value: nothing to restore.
            this.value = "";
          }
        });
      });
    }
    
    // Install input filters.
    setInputFilter(document.getElementById("intTextBox"), function(value) {
      return /^-?\d*$/.test(value);
    }, "Must be an integer");
    setInputFilter(document.getElementById("uintTextBox"), function(value) {
      return /^\d*$/.test(value);
    }, "Must be an unsigned integer");
    setInputFilter(document.getElementById("intLimitTextBox"), function(value) {
      return /^\d*$/.test(value) && (value === "" || parseInt(value) <= 500);
    }, "Must be between 0 and 500");
    setInputFilter(document.getElementById("floatTextBox"), function(value) {
      return /^-?\d*[.,]?\d*$/.test(value);
    }, "Must be a floating (real) number");
    setInputFilter(document.getElementById("currencyTextBox"), function(value) {
      return /^-?\d*[.,]?\d{0,2}$/.test(value);
    }, "Must be a currency value");
    setInputFilter(document.getElementById("latinTextBox"), function(value) {
      return /^[a-z]*$/i.test(value);
    }, "Must use alphabetic latin characters");
    setInputFilter(document.getElementById("hexTextBox"), function(value) {
      return /^[0-9a-f]*$/i.test(value);
    }, "Must use hexadecimal characters");
    .input-error {
      outline: 1px solid red;
    }
    <h2>JavaScript input filter showcase</h2>
    <p>Supports Copy+Paste, Drag+Drop, keyboard shortcuts, context menu operations, non-typeable keys, the caret position, different keyboard layouts, and <a href="https://caniuse.com/#feat=input-event" target="_blank">all browsers since IE 9</a>.</p>
    <p>There is also a <a href="https://jsfiddle.net/emkey08/tvx5e7q3" target="_blank">jQuery version</a> of this.</p>
    <table>
      <tr>
        <td>Integer</td>
        <td><input id="intTextBox"></td>
      </tr>
      <tr>
        <td>Integer &gt;= 0</td>
        <td><input id="uintTextBox"></td>
      </tr>
      <tr>
        <td>Integer &gt;= 0 and &lt;= 500</td>
        <td><input id="intLimitTextBox"></td>
      </tr>
      <tr>
        <td>Float (use . or , as decimal separator)</td>
        <td><input id="floatTextBox"></td>
      </tr>
      <tr>
        <td>Currency (at most two decimal places)</td>
        <td><input id="currencyTextBox"></td>
      </tr>
      <tr>
        <td>A-Z only</td>
        <td><input id="latinTextBox"></td>
      </tr>
      <tr>
        <td>Hexadecimal</td>
        <td><input id="hexTextBox"></td>
      </tr>
    </table>

    TypeScript

    Here is a TypeScript version of this.

    function setInputFilter(textbox: Element, inputFilter: (value: string) => boolean, errMsg: string): void {
      ["input", "keydown", "keyup", "mousedown", "mouseup", "select", "contextmenu", "drop", "focusout" ].forEach(function(event) {
        textbox.addEventListener(event, function(this: (HTMLInputElement | HTMLTextAreaElement) & { oldValue: string; oldSelectionStart: number | null, oldSelectionEnd: number | null }) {
          if (inputFilter(this.value)) {
            this.oldValue = this.value;
            this.oldSelectionStart = this.selectionStart;
            this.oldSelectionEnd = this.selectionEnd;
          }
          else if (Object.prototype.hasOwnProperty.call(this, "oldValue")) {
            this.value = this.oldValue;
            
            if (this.oldSelectionStart !== null &&
              this.oldSelectionEnd !== null) {
              this.setSelectionRange(this.oldSelectionStart, this.oldSelectionEnd);
            }
          }
          else {
            this.value = "";
          }
        });
      });
    }
    

    jQuery

    There is also a jQuery version of this. See this answer.

    HTML5

    HTML5 has a native solution with <input type="number"> (see the specification and documentation). The documentation has a working demo of this input type.

    This approach fundamentally has a different user experience: you are allowed to input invalid characters and the validation is performed separately. This has the benefit that the undo stack (CtrlZ) won’t break. Note that server-side validation must be performed, regardless, no matter which approach you choose.

    But note that browser support varies:

    Demo

    document.querySelector("form").addEventListener("submit", (event) => {
      event.preventDefault();
      console.log(`Submit!
      Number is ${event.target.elements.number.valueAsNumber},
      integer is ${event.target.elements.integer.valueAsNumber},
      form data is ${JSON.stringify(Object.fromEntries(new FormData(event.target).entries()))}.`);
    })
    label {
      display: block;
    }
    <form>
      <fieldset>
        <legend>Get a feel for the UX here:</legend>
        <label>Enter any number: <input name="number" type="number" step="any" required></label>
        <label>Enter any integer: <input name="integer" type="number" step="1" required></label>
        <label>Submit: <input name="submitter" type="submit"></label>
      </fieldset>
    </form>