DEV Community

Cover image for Input Mask Algorithm
Caleb Lewis
Caleb Lewis

Posted on

Input Mask Algorithm

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:

demo

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, see targetChar) any character can be used to represent phone numbers, dates, etc.
  • targetChar: the character in template 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
}
Enter fullscreen mode Exit fullscreen mode

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
) {
    // ...
}   
Enter fullscreen mode Exit fullscreen mode

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])
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Finally, we return the output as a string:

return masked.join('')
Enter fullscreen mode Exit fullscreen mode

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('-', '')
Enter fullscreen mode Exit fullscreen mode

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('')
}
Enter fullscreen mode Exit fullscreen mode

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)