Digit Only Directive in Angular

Changhui Xu
codeburst
Published in
5 min readSep 26, 2018

--

Natural numbers are frequently used as Account Numbers, Transaction IDs, Job Codes, etc. All these numbers are critical to identify domain models and ensure data integrity. We often get requirements that the input boxes for entering these numbers should only allow digits (0,1,2,3,4,5,6,7,8,9) in order to avoid human errors. This post will describe an Angular directive digitOnly that we are using to make input boxes only accept digits. The ideas in this directive are also applicable to other frameworks.

The source code is available in this GitHub repo and I have published it as an npm package (@uiowa/digit-only). With the help of Angular directive, input box will only allow [0–9] when typing, pasting or drag/dropping. Moreover, this directive works on keyboards for either Windows or Mac computers. You can try it out at this demo site.

Can we use <input type=”number”> ?

Before jumping into the code, let’s discuss an alternative solution, <input type="number">. For the use cases discussed here, we don’t want to show the number spinner and we can modify CSS to remove the number spinner as follows.

input[type="number"] {
-moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}

However, as described in this article by Ollie Williams, number input doesn’t make sense in cases like credit card numbers or zip codes. The main drawback of number input is the inability to apply the maxlength attribute.
We can use inline JavaScript to remedy the maxlength issue in number input. For example,

<input type="number" name="zipcode" 
maxlength="5" placeholder="00000"
oninput="javascript: if (this.value.length > this.maxLength) this.value = this.value.slice(0, this.maxLength);" >

This solution almost meets our needs. However, due to the nature of numbers, the number input box accepts values containing decimal points and number values in scientific notation. For example, you can input “1.00” as a number in this w3schools example, and the input box doesn’t complain. More interestingly, a number in scientific notation, like “1e+1”, equals 10, is a valid number input value, which is definitely not what we want.

To sum up, number input is not appropriate for use cases described above.

Can we use <input type=”text” pattern=”[0–9]*”>?

HTML5 introduces the pattern attribute to check an <input> element’s value. The pattern attribute works with the following input types: text, date, search, url, tel, email, and password. With the help of the pattern attribute, we can have an input element as follows.

<input type="text" pattern="[0-9]*" maxlength="3"
name="country_code" title="Three digits country code">

The above input box will validate an input value based on the provided regular expression when a user submits a form. Note that the default appearance of the validation message depends on browsers, which you can customize CSS to style a uniform look across browsers. Or you can ignore HTML5 form validation and use form validations in front-end frameworks.

This solution is slightly better than the above one using number input. The good side of this solution is simple and easy. However, you might have noticed that the input type text accepts letter keystrokes and accepts any pasted text. Even though form validation will eventually catch the erroneous input value, making users go back and fix the input is not a good experience.

Angular Directive to the rescue

After the journey above, we arrive at the route of using a directive. Again, the idea in this directive applies to all frameworks. There is a concept of “mask” for text input to do a similar job. For example, you can specify “credit card” mask in Vuetify text fields. We noticed one minor flaw in Vuetify though. When you copy & paste a string to a credit card input field (example) or keep typing letters, you will notice a flash of string being removed from the input field.

The goal of this directive is to discard non-digit keystrokes when users are typing and to remove non-digit characters when users paste or drag/drop text in an input box. So the directive needs to handle 3 events: keydown, paste and drop. (source code)

First, let’s take a look at the keyboard event listener. To handle keyboard events, we need to be cautious that keycodes are different in Mac and Windows. Also we need to consider that number keys have different keycodes in the main keyboard and the number pad. The code snippet below is adopted from StackOverflow answers with some improvements.

@HostListener('keydown', ['$event'])
onKeyDown(e: KeyboardEvent) {
if (
// Allow: Delete, Backspace, Tab, Escape, Enter, etc
this.navigationKeys.indexOf(e.key) > -1 ||
(e.key === 'a' && e.ctrlKey === true) || // Allow: Ctrl+A
(e.key === 'c' && e.ctrlKey === true) || // Allow: Ctrl+C
(e.key === 'v' && e.ctrlKey === true) || // Allow: Ctrl+V
(e.key === 'x' && e.ctrlKey === true) || // Allow: Ctrl+X
(e.key === 'a' && e.metaKey === true) || // Cmd+A (Mac)
(e.key === 'c' && e.metaKey === true) || // Cmd+C (Mac)
(e.key === 'v' && e.metaKey === true) || // Cmd+V (Mac)
(e.key === 'x' && e.metaKey === true) // Cmd+X (Mac)
) {
return; // let it happen, don't do anything
}
// Ensure that it is a number and stop the keypress
if (e.key === ' ' || isNaN(Number(e.key))) {
e.preventDefault();
}
}

With the implementation above, the host element will (1) ignore decimal points and other keystrokes except digits, and (2) still allow common functional keys in either Mac or Windows.

Then we add the other two event handlers. The two methods below intercept ClipboardEvent and DragEvent to modify the text, so that digit-only strings are inserted into the host element.

@HostListener('paste', ['$event'])
onPaste(event: ClipboardEvent) {
event.preventDefault();
const pastedInput: string = event.clipboardData
.getData('text/plain')
.replace(/\D/g, ''); // get a digit-only string
document.execCommand('insertText', false, pastedInput);
}
@HostListener('drop', ['$event'])
onDrop(event: DragEvent) {
event.preventDefault();
const textData = event.dataTransfer
.getData('text').replace(/\D/g, '');
this.inputElement.focus();
document.execCommand('insertText', false, textData);
}

Very good. All infrastructure has been established and we use a selector digitOnly to bind this directive to the host element. The code snippet below shows an example usage.

<input type="text" digitOnly placeholder="000" maxlength="3">

Simple enough and we meet customers’ needs. Of course, server side validation is always a MUST.

Numeric keypad?

You may ask, how to pull out the numeric keypad in mobile devices and tablets? The solution might change as iOS and Android evolves. Currently, the best solution is the combination of pattern and inputmode attributes in <input> element. For instance, you can add inputmode=”numeric” pattern=”[0–9]*” to input element as below.

<input type="text" name="creditcard" id="creditcard_number" 
placeholder="000" maxlength="3"
inputmode="numeric" pattern="[0-9]*" digitOnly>

That’s all for today. And again, source code is available at this GitHub repo and you can also try out the demo site. Feel free to leave comments below. Thank you.

✉️ Subscribe to CodeBurst’s once-weekly Email Blast, 🐦 Follow CodeBurst on Twitter, view 🗺️ The 2018 Web Developer Roadmap, and 🕸️ Learn Full Stack Web Development.

--

--

Lead Application Developer. MBA. I write blogs about .NET, Angular, JavaScript/TypeScript, Docker, AWS, DDD, and many others.