And you’re thinking that building an input mask is not that hard. All you need to do is create a bunch of functions to validate the key using keyboard events and cancel the event when the key is not allowed. How hard can it be?
Famous last words.
It took me about 10 seconds to remember that keyboard events work differently for different devices. Turns out, finding a solution to make an input mask work in Android, iOS and Google Chrome would not be that easy. So yeah, my confidence was short-lived.
Introducing Keyboard Events
Let me introduce you to keyboard events. They are triggered by interacting with a physical or virtual key:
- Pressing down a key triggers the keydown event.
- Pressing down a printable key triggers the keypress event.
- Releasing a key triggers the keyup event.
Whenever a keyboard interaction happens, the event will provide an object with the information about the interaction itself. For example, what key was pressed and the physical location on the keyboard. Here are some of the properties that can be used to identify the pressed key:
- keyCode returns a numeric value associated with a particular pressed key no matter if this key is in lowercase or uppercase.
- charCode indicates the ASCII value of the character associated with the pressed key. This property distinguishes between lowercase and uppercase.
- key returns the value of the pressed key. If you press the lowercase a key this property will return the letter a, instead of a code representation of the key.
- which returns the numeric keyCode of the key pressed or the charCode for an alphanumeric key.
- keyIdentifier returns a string representation of the pressed key.
- code represents a physical key on the keyboard. This property returns a value that isn’t altered by the keyboard layout or the state of the modifier keys.
Introducing (Paranoid) Android Problems
Well, now that we know what it takes to trigger a keyboard event and how to identify the key that was pressed, it should be a snap to do the necessary validations for the mask. Right?
The thing is Android keyboard events are not that simple. When I added the keypress event and printed the keyCode property, I had a little surprise: there was no keypress in Android.
With keypress, the uppercase A resulted in the respective keyCode in the following browsers:
I quickly found out why Chrome Mobile wasn’t triggering the event: this event is marked as Legacy in the DOM-Level-3 Standard. Okay, so this is not the end of the world. I can still use one of the two events left: keydown or keyup.
I tested both events. Pressing the uppercase A resulted in the following keyCodes:
OK, the event was fired, but what was that 229 code? Where did it come from? Well, after some searching, I learned that the auto-suggest feature or other event might follow the keydown event and invalidate it. Some devices can return the code 0, Unidentified or even empty, but the reason is the same.
And, to make things more interesting, disabling the auto-suggest feature does not solve the problem.
To help me decide which property I should use, I went into testing mode and created the following table. I know some of the following properties are deprecated but what can I tell you? I was bitten by the testing bug!
I tested those events by myself in my devices, but, if you need more information about the properties, versions, and compatibility you can check out the MDN website.
As you can see, no property works on Chrome Mobile, but hey, don’t panic yet! I have a workaround. Yes, after a great deal of searching and testing, I found a good solution to obtain the key that was pressed in both iOS and Android. But first, let’s talk about one more event and the order all these events are triggered. Bear with me.
Introducing the Input Event
The input event is fired whenever we type something, meaning the value of an <input>, <select>, or <textarea> element is changed. The event is fired in all listed browsers. But, it doesn’t return the same properties as the keyboard events, because this event is not triggered by keyboard actions. Instead it’s triggered by the result of those actions. You can check the Mozilla Developer Network website to see its properties.
When we press a key and then release it, there are four events triggered in the following order:
keydown > keypress > input > keyup
Note that triggering the keydown event changes the value, raising the input event. Only after the key is released is the keyup event fired. Remember that the keypress event may not trigger in some devices.
This is How You Handle Android keyCode 229
Now that you know the input event and the order of events, let’s talk about the workaround for the Android 229 bug.
Here’s how to find the pressed key. When the keydown event is triggered, the input value doesn’t change and so you should store it then. However, if the event isn’t canceled, the input value will change. This triggers the input event and creates a new value. So if you had stored the input value before the change, you could then compare it with the new one after the change.
For example, if you have an input with an 11 value, and you press the A key, the new value will be 11A. Compare both values and you will get the A.
If you really really really need to use the charCode, you can still get the character that is the difference between oldValue and newValue and use the charCodeAt function.
These Are the Requirements for the Input Mask
OK, so now that we’ve got a few things out of the way, and we are able to trigger the events and get the pressed key, we can actually start building the mask. But which mask?
The credit card mask was one required for my project. This mask is the simplest one to explain, so it’s the one I’ll be talking about from now on.
So let’s analyze its requirements:
- Allow only 16 digits
- Add the separator “ “ (space) for every group of 4 digits
- Should work with copy-paste values
- If the pasted value has unallowed keys, the mask should remove those keys and mask only the digit part that remains
Looking through the requirements, note that the input mask should treat the complete value whenever it changes instead of the last inserted key. Wait a minute! So, did we work on getting the typed value for nothing? No! We are not going to use the difference between the old value and the new one to validate keys, but we are going to use the old value anyway. Why? Keep reading!
Let’s do what we already know: Add both onkeydown and oninput event listeners to the input and store the old and the new value.
Credit Card Input Mask: Let’s Get Coding!
For this case, we are going to create and work with two functions: one to mask the input value and another one to unmask it, and we’re gonna call them in this order.
But why do we have to unmask the value and then mask it again?
Well, imagine the following scenario: an input has 1111 2222 3333 as a value, and then the user selects the entire value and replaces it with a new one, for example: 444455 55. This new value is masked regardless of its structure. It will always adapt to the mask, clearing all characters that are not digits.
The Mask Function
This function must add separators between every group of four digits. To find it, you need to check whether the remainder of the division (represented by %) of the index/interval (four groups of four digits) is equal to zero. Read more about the modulus operator.
Here’s what the function looks like:
Example: If the new value is 11112, the calculation will be:
According to the table above, the separator will be added to the first position. But, this is not what I want for my mask, so I had to exclude the index 0 from the modulus calculation.
The Unmask Function
This function is simpler. It prepares the value to be masked again. It implies replacing every non-digit character to “” (empty value).
This is what the unmask function looks like:
The credit card mask only allows 16 digits. To limit the character insertion, we are going to create a new regex rule and check if the unmasked value matches the regex. If true, the value can be masked and printed again. If false, the value is the old one.
Your code should look like this:
If you test the mask you will see that it works.
Let’s talk about the two input properties:
- selectionStart: Get the start of the selected portion of the field’s text.
- selectionEnd: Get the end of the selected portion of the field’s text.
You may be asking yourself why we are using selection to get the cursor position. If you don’t have any text selected, the starting point of a selection will be the cursor position, and the ending point will be the cursor position as well.
So here’s what you do. Create a new variable and call it oldCursor.
Inside the keydown event, store the cursor position using selectionEnd before the insertion of the new value. Now you have both oldValue and oldCursor containing information about the value and the cursor position, respectively, before they change.
Create a new function that will check how many separators the string has until it reaches the cursor position. In this example I called it checkSeparator.
This function will return the rounded value of the position/interval (4) + 1, where the +1 is the compensation of the separator. In other words, return +1 for every separator before the position.
I built a formula to set a new cursor position based on the current cursor position, the oldValue and the newValue.
Now that you already have the new cursor position, you can apply it to the input using the
setSelectionRange(start, end) method. It sets a selection between the start and end parameters. If the start and end values are the same, there will be no interval between the selection.
Keyboard Types in Mobile Applications
When you type a credit card number, the keyboard must be the numerical one. If you test the mask in your device, you will have the alphanumeric keyboard at your disposal. This happens because the input type is text and the input type defines which virtual keyboard type is displayed. My first thought was to change the input type to number, so that the HTML would force the keyboard to be the numerical one.
Was I right? Nope, I was wrong! The number type input does not allow our separators and it automatically runs validations that we don’t want. So, how did I solve this? Well I went for a solution that I was trying to avoid because it’s not so elegant. I tried “tel” type.
By using the “tel” type, the keyboard is the numerical one, and the input allows special characters such as our separator. And, there won’t be any validations on the input value.
Yes, it worked! And best of all, it works on Android and iOS devices.
See it working beautifully here:
The Output of the Input Mask
As you can see, creating an input mask is no walk in the park. What I thought would be a nice relaxing stroll ended up being one long marathon in the forest. At night. During the new moon. So it was dark. And I was alone. OK, you get it.
My input mask adventure was a long, stressful and painful process of trying, failing, researching and doing it all over again. Having learned there is no simple way to reach some of the properties of keyboard events, I can tell you I now have such respect for them. They require your fullest attention, regardless of why you need them. So I have to admit, now whenever someone asks me something related to keyboard events, I take it very seriously.
Here’s the full code of this little input mask project by the way, so you can see how serious I am: