What's an Input Mask?
An input mask is a template used to constrain a user's input. It can be used to create
nicer form experiences by formatting their input as the user types:
Algorithm
Here are the pieces of information we need:
-
input
: the input from the user -
template
: the format we want the user's input to be in. (Almost, seetargetChar
) any character can be used to represent phone numbers, dates, etc. -
targetChar
: the character intemplate
that we'll replace with the user's input as they type
To solve, we'll use a straightforward approach: iterate through the input
and template
strings, replacing
targetChar
until one of them finishes first.
Let's create our masking function:
function mask(input, template, targetChar) {
const output = [] // final masked result
let templateIndex = 0 // template pointer
let inputIndex = 0 // input pointer
}
We'll need different index pointers for input
and template
because their lengths and
the targetChar
locations are arbitrary, so their indexes will need to be aligned manually
as we iterate through the strings.
We want to keep track of our offsets so that we can easily manipulate both offsets independently.
On a given iteration, we don't want to increment the offset if we didn't replace a character from
the mask (when mask[maskIndex] !== maskChar
).
According to our approach, our stopping condition is when we've reached the end of either
input
or template
strings:
while(
inputIndex <= input.length &&
templateIndex <= template.length
) {
// ...
}
At each iteration, we'll check whether the character at templateIndex
is
targetChar
to decide whether we choose to add to the input from template
or from input
:
if (template[templateIndex] === targetChar) {
output.push(input[inputIndex])
} else {
output.push(template[templateIndex])
}
Next, we need to align the indexes.
Normally, we'd always want to go to
the next character at the end of the iteration, however we don't want to
move on if a character isn't used. A character from the template is always added to the output (input
is technically part of the template
)
, but the input character only gets used when a targetChar
is reached.
Therefore, we'll only increment inputIndex
if we find targetChar
and increment templateIndex
at every iteration:
if (template[templateIndex] === targetChar) {
output.push(input[inputIndex])
inputIndex++ // increment input
} else {
output.push(template[templateIndex])
}
templateIndex++ // increment template
Finally, we return the output as a string:
return masked.join('')
When using a mask, it's often beneficial to use two inputs:
- a visible one using the masked value to show to the user
- an invisible one that holds the raw value that the user never sees
The raw value can be retrieved by removing any character that isn't targetChar
from the template:
// targetChar: '#', template: '###-###-####'
const rawInput = maskedInput.replaceAll('-', '')
Here's the whole function:
function mask(input, template, targetChar) {
input = input.replaceAll('-', '')
const output = []
let templateIndex = 0
let inputIndex = 0
while(inputIndex <= input.length && templateIndex <= template.length) {
if (template[templateIndex] === targetChar) {
output.push(input[inputIndex])
inputIndex += 1
} else {
output.push(template[templateIndex])
}
templateIndex += 1
}
return masked.join('')
}
Closing thoughts
It's fun to think about how extensible this can be; there can be a polymorphic overload
that accepts a function as a template to provide dynamic masks, or this would even be
great as a sort of input middleware such that it's just one transformation in a set of
other operations since it's atomic.
I ran into this problem while working on a bigger article on implementing phone login with Firebase and
thought I'd share this fun little exercise. Stay tuned for the other article!
Follow me on Twitter
bonus: first person to prove they shipped this copy+pasted code gets $5 👀
Top comments (0)