11

I have a html <input> element that I want to accept only numbers and to be recognised on mobile devices as a number field. I also want invalid characters to be swallowed, just like for standard type=number swallowing disallowed characters.

I've tried the obvious type=number but it has a number of shortcomings. Specifically it allows 'e', '+' and '-' (at least in chrome), but these were easy to fix with some JS. The real problem is with the '.' character, I want to be able to enter floating point numbers e.g. '0.10', '5.5054', but don't want to be able to enter invalid strings like '0.10.1' for instance. I tried to resolve this by allowing only 1 '.' at a time but this failed as the input.value gets massaged by the browser e.g. '5.' becomes '5', '5..' becomes null (!) and it seems impossible to get the raw string value typed in the input. The above means checking for existing '.'s and taking action appears to be a dead end...

Core questions:

  1. Is there a way I missing to inspect and conform the input?
  2. 'Is there a way of marking an input as a number without the logistical baggage of type=number?

Note:
* I realise that you can paste whatever you want in, I consider that behaviour pathological and shouldn't be covered by input prevention.

Update

To clarify, I have already tried keypress, keydown etc events and they aren't adequate as I want to see how many '.'s exist in the input currently to choose whether or not to allow another. At this point the input.value has been massaged by the browser to remove '.'s. I want to conditionally allow characters based on the current number of '.'s that have been entered.

Example

HTML (angular style binding for brevity)

<input type="number" (keydown)="keyDown()">

JS

function keyDown($event: KeyboardEvent) {
  const inputField = // obtain reference to input element
  const value = inputField.value;
  if ( value.indexOf('.') !== -1 && $event.key === '.') { // disallow another . if one is present
    // ! input field prunes . so this check isn't sufficient
    $event.preventDefault();
    return;
  }

  // This is the crux of the problem e.g.
  // type 5. into input field, value === 5
  // type 5.. into the input field, value === null
  // Since the . char is removed by the input element there's no way to know how many are present!
  console.log(value);
}

Summary

  1. Is there a way to signal that an <input> is of type number without using the type=number attribute setting.
    1. i.e. mobile devices recognise and display number pad etc
  2. For an <input> that has type=number is there a way to swallow all key input that doesn't result in a valid number
    1. Before the character is added to the input by the browser, no janky deletion on keyup
halfer
  • 19,824
  • 17
  • 99
  • 186
radman
  • 17,675
  • 11
  • 42
  • 58
  • How, or do you, want to cater for cultures that use "," (or other) as the [decimal indicator](https://en.wikipedia.org/wiki/Decimal_separator). Also please show what you have tried so we don't spend out time on an approach you have already ruled out. – Jon P Sep 02 '19 at 02:10
  • @JonP, I'll get a cut down code example posted up to illustrate. As far as "," goes I'm not fussed, the core of my problem isn't about the particular char, I can massage that part of it pretty easily. – radman Sep 02 '19 at 05:19
  • @JonP, code added, let me know if anything is unclear. – radman Sep 02 '19 at 05:45
  • Would inputmode="decimal" satisfy the requirement for type="number" for mobile? https://inputtypes.com/ – ctaleck Sep 03 '19 at 04:31
  • @ChristopherTaleck, that looks to be EXACTLY what I'm after :) With that I can use `type=text` and all my issues go away. Post this as an answer and I will hook you up! – radman Sep 04 '19 at 01:11

8 Answers8

15

Is there a way to signal that an <input> is of type number without using the type=number attribute setting. i.e. mobile devices recognise and display number pad etc

Use inputmode="decimal" instead of type="number" to signal a mobile device to use a number pad keyboard input. This way you can continue to use type="text" and process the input as needed.

See MDN for more info and inputtypes.com to test on a device.

ctaleck
  • 1,658
  • 11
  • 20
  • This is a good article detailing the travails with `type=number` and `inputmode`, still not a no brainer but will get the job done. https://www.filamentgroup.com/lab/type-number.html – radman Sep 04 '19 at 04:24
3

A slightly different approach. It allows digits, only 1 period, and backspace. All the rest of KeyboardEvent.keys including ctrl + v and ctrl + c are ignored. But if wish to allow them, you can do so.

To check if the character is one of the 10 digits, I am using event.key since they can have two different codes: Digits[0-9] and Numpad[0-9]. But for the period and backspace, I am using event.code since they have only one code.

const input = document.querySelector("#number_input");

const App = {
  isDigit: function(key) {
    const digits = [
      "0",
      "1",
      "2",
      "3",
      "4",
      "5",
      "6",
      "7",
      "8",
      "9"
    ];
    return digits.includes(key);
  },
  isPeriod: function(code) {
    return code === "Period";
  },
  isBackSpace: function(code) {
    return code === "Backspace";
  },
  handleEvent: function(event) {
    const key = event.key;
    const code = event.code;
    const value = input.value;
    if (App.isDigit(key) || App.isPeriod(code) || App.isBackSpace(code)) {
      if (App.isPeriod(code) && value.indexOf(key) !== -1) {
        event.preventDefault();
      }
    } else {
      event.preventDefault();
    }
  }
};

input.onkeydown = App.handleEvent
<input id="number_input" />

A clever hack

Since you insist to use a number input. First use, a dummy text input which you can hide it using either CSS or Js and validate its value instead of the number input.

const input = document.querySelector("#number_input");
const dummyInput = document.querySelector("#dummy_input")
const App = {
  isDigit: function(key) {
    const digits = [
      "0",
      "1",
      "2",
      "3",
      "4",
      "5",
      "6",
      "7",
      "8",
      "9"
    ];
    return digits.includes(key);
  },
  isPeriod: function(code) {
    return code === "Period";
  },
  isBackSpace: function(code) {
    return code === "Backspace";
  },
  handleEvent: function(event) {
    const key = event.key;
    const code = event.code;
    const dummyValue = dummyInput.value;
    if (App.isBackSpace(code)) {
      dummyInput.value = dummyValue.substring(0, dummyValue.length - 1)
    } else {
      if (App.isDigit(key) || App.isPeriod(code)) {
        if (App.isPeriod(code) && dummyValue.indexOf(key) !== -1) {
          event.preventDefault();
        } else {
          dummyInput.value += event.key
        }
      } else {
        event.preventDefault();
      }
    }
  }
};

input.onkeydown = App.handleEvent
<input type="number" id="number_input" />
<input type="text" id="dummy_input" />

Update

All of the answers that use input[type="number"] have a problem. You can change the input's value to a negative number by mouse wheel/spinner. To fix the issue, set a minimum value for the input.

<input type="number" min="1" id="number_input" />

You need to listen for onchange events and then change value of the dummy input.

const input = document.querySelector("#number_input");
const dummyInput = document.querySelector("#dummy_input")
const App = {
  isDigit: function(key) {
    const digits = [
      "0",
      "1",
      "2",
      "3",
      "4",
      "5",
      "6",
      "7",
      "8",
      "9"
    ];
    return digits.includes(key);
  },
  isPeriod: function(code) {
    return code === "Period";
  },
  isBackSpace: function(code) {
    return code === "Backspace";
  },
  handleEvent: function(event) {
    const key = event.key;
    const code = event.code;
    const dummyValue = dummyInput.value;
    if (App.isBackSpace(code)) {
      dummyInput.value = dummyValue.substring(0, dummyValue.length - 1)
    } else {
      if (App.isDigit(key) || App.isPeriod(code)) {
        if (App.isPeriod(code) && dummyValue.indexOf(key) !== -1) {
          event.preventDefault();
        } else {
          dummyInput.value += event.key
        }
      } else {
        event.preventDefault();
      }
    }
  },
  handleChange: function(event) {
    dummyInput.value = event.target.value
  }
};

input.onkeydown = App.handleEvent;
input.onchange = App.handleChange;
<input type="number" min="1" id="number_input" />
<input type="text" id="dummy_input" />
mahan
  • 12,366
  • 5
  • 48
  • 83
  • The input must have attribute `type=number`, you'll find the behaviour very different with that in place. – radman Sep 02 '19 at 08:02
  • I required `type=number` so that the field is recognised as being a number field. Allowing mobile devices to show a number pad etc. If you re read my question you'll see that I say that in the first sentence AND that if there is a way of signalling this without `type=number` then I would consider that a valid solution. – radman Sep 03 '19 at 01:13
  • does the dummy approach allow for the field to be recognised as being of type number for mobile devices? – radman Sep 03 '19 at 02:11
  • @radman Of course. You do not change any properties of the `input[type="number"]`. This is similar to https://stackoverflow.com/questions/54147009/toggle-accordion-glyph-icon-with-css-only/54147692#54147692. The other input is used just to intercept keyboard events and for its validation. – mahan Sep 03 '19 at 06:54
0

trigger special function for onkeypress and check if your allowed character are typed or not. If not - prevent the default behavior.

let special = document.getElementById('special_input');
special.addEventListener("keypress", function(e) {
  let dot = 46;
  // allowed char: 0-9, .
  let allow_char = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, dot];
  if (allow_char.indexOf(e.which) !== -1) {
    // only 1 dot
    if (e.which == 46 && special.value.indexOf('.') !== -1)
      e.preventDefault();
  } else {
    e.preventDefault();
  }
});
<input id='special_input'>
Omri Attiya
  • 3,917
  • 3
  • 19
  • 35
  • Thanks for the attempt but if you run your code in the browser you'll see that you can input as many '.'s in a row as you want, the failure of this basic approach is the core of my question. – radman Aug 30 '19 at 06:57
  • 1
    @radman, Omri's answer seems to work fine on Google Chrome. What browser are you testing this in? – Kei Sep 02 '19 at 02:17
  • @Kei, I assume all the posted answers work, but they don't address my question. Filtering keypresses is a trivial problem, my issue is that I can't inspect the value of the input. "type=number" fields don't present the raw text, they parse and give you a number, or not depending. Please read my question and understand the problem. – radman Sep 02 '19 at 05:16
0

updated

The answer is updated as per requirement changes.

okay I try to fix one more problem and that is paste

according to following code you can not paste anything, you can only paste numbers, if you try to paste string, the input box will become empty automatically :)

  let special = document.getElementById('inputField');
  special.addEventListener("keypress", function(e) {
  let dot = 46;
  // allowed char: 0-9, .
  let allow_char = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, dot];
  if (allow_char.indexOf(e.which) !== -1) {
    // only 1 dot
    if (e.which == 46 && special.value.indexOf('.') !== -1)
      e.preventDefault();
  } else {
    e.preventDefault();
  }
});

function checkString()
{
  setTimeout(() => {
  var value = document.getElementById("inputField").value;
  value = parseInt(value);
    if(isNaN(value))
      document.getElementById("inputField").value = "";
    }, 100);

}
  <input type="number" id="inputField" onpaste="checkString()"/>
Dupinder Singh
  • 7,175
  • 6
  • 37
  • 61
0

Check truthy with

value == parseFloat(value)

parseFloat() will capture the first decimal point and remove any additional ones.

Using '==' instead of '===' will make sure the reference value isn't changed.

function keyDown($event: KeyboardEvent) {
    const inputField = // obtain reference to input element
    const value = inputField.value;
    if ( value == parseFloat(value)) { // disallow another . if one is present
      // ! input field prunes . so this check isn't sufficient
      $event.preventDefault();
      return;
    }

    // This is the crux of the problem e.g.
    // type 5. into input field, value === 5
    // type 5.. into the input field, value === null
    // Since the . char is removed by the input element there's no way to know how many are present!
    console.log(value);
  }

Here is a similar test you can run in node.js to verify it accomplishes what you are going for.

function numCheck (value) {
    if (value == parseFloat(value)) { 
        console.log(value)
    } else {
        console.log('not a number')
    }
}

numCheck("5");
numCheck("5.5");
numCheck("5.5.5");
numCheck("A");
-1

For restrictions, Regex is best way to go. Register event listener on keypress

numbersOnly(event) {
    const numbers = /^([0-9])$/;
    const result = numbers.test(event.key);
    return result;
}

Above function will swallow charset, if it doesn't match regex

Javascript Regex: validating a double/float

Above link has regex, who allows double/float. Hope it helps :)

Victor
  • 379
  • 3
  • 13
  • Thanks for the attempt but if you read my question more carefully you'll see I've already tried this approach. – radman Aug 30 '19 at 06:56
-1

Perhaps an indirect solution to your problem. You can look into validity property to determine if the input is valid. You can check the validity without the need to access the value, which may not be returned in case of invalid input.

HTML:

<input type="number" id="num" step="0.1"/>

JS:

var numField = document.getElementById('num');
numField.addEventListener("keyup", event => {
   if(!numField.validity.valid)
   {
      alert('invalid input');
   }
});

Some tests:

34    -> valid
3.4   -> valid
e     -> invalid
+4    -> valid    
3.4.  -> invalid
3.4.. -> invalid
.4    -> valid (evaluates to 0.4)
3e+2  -> valid
1e    -> invalid
eee   -> invalid
-0.3e+1 -> valid
..32e.. -> invalid

This works both with pasting and inputting the values.

UPDATE:

The original question is a bit verbose. I am trying to break it down and answer part by part.

Input[type="number"] allows access to the value only if it is valid and returns empty otherwise. Thus, you can know if value is correct or not, however you cannot get the invalid value.

You could get around this by pushing individual keypresses into a buffer. However, maintaining sync between the buffer and actual value is challenging due to copy/paste, input field swallowing certain keys, moving the cursor around and inserting keys, etc. Implementing reliable cross-platform support for all of this seems like overkill. A better approach would be to implement other means to input the number, e.g. a slider. This resource covers the basics of designing number inputs

So, you are left with knowing if the current input is valid and the last key pressed. The problem is that certain invalid inputs, e.g. "4." may lead to a valid input "4.1". Thus, just swallowing the last key in case the input turns invalid is out of the question.

You could implement a rule that if invalid input is not turned into a valid one within 2 keystrokes (or 1 second) the input returns to last valid state swallowing the invalid input. However, I would argue that this creates UX problems because users may not be used to such behavior and it opens doors for users submitting technically valid, yet unwanted values.

Core questions:

  1. Is there a way I missing to inspect and conform the input?

  2. Is there a way of marking an input as a number without the logistical baggage of type=number?

  1. Apparently not.

  2. Yes, there is by rethinking the way how you enter numeric values.

Eriks Klotins
  • 4,042
  • 1
  • 12
  • 26
  • I hate to be a rampant downvoter, but seriously; please read the question! Your answer doesn't address anything I've asked :P I'm interested in swallowing input not checking validity. This answer would've worked better as a comment. – radman Sep 03 '19 at 01:16
  • Please look at the update. Is my thinking is on the right track? – Eriks Klotins Sep 03 '19 at 08:41
  • you're definitely on a better track now, though I've covered much of the ground already. The suggestion of a number slider is interesting as an 'outside the box' proposal but it's not a viable answer to my question unfortunately. – radman Sep 04 '19 at 07:20
  • I am afraid you cannot do much more than that with built-in functionality – Eriks Klotins Sep 04 '19 at 08:06
-1

This may be what your looking for:
This input eats all caracters exept for numbers and "."

let prev = "";
function keyUp(event) {
    if ((!event.target.value && event.keyCode !== 8) || event.target.value.indexOf('e') !== -1 || parseFloat(event.target.value) == NaN) {
        event.target.value = prev;
    } else {
        prev = event.target.value;
    }
}
<input type="number" onkeyup="keyUp(event)">
Tobias Fuchs
  • 910
  • 5
  • 8
  • Please read the question, you've missed critical parts of it. Also since your example is using `keyup` deletions will be visible to the user, chars immediately appear and are removed, not desirable UX. – radman Sep 03 '19 at 02:13