Masked Inputs In Aurelia: The Easy (and reliable) Way

When it comes to adding in masked inputs into a modern Javascript web application, it is easier said than done. The task at hand is simple, yet, under the surface is paved with complexity in a framework with unidirectional data flow.

The problem I am going to describe is also a problem you’ll encounter in Angular, Ember, Vue and any other framework or library which offers two-way binding on input elements or modifying the input itself.

The Problem

You have an input element. You want users to be able to enter a value and automatically format it as they type. This input could be anything, but in my situation, the input was for entering numeric values.

I wanted the value to automatically add a comma for the hundreds, add two decimals to the end and a dollar sign ($) symbol as a prefix to the value.

By default in Aurelia, binding on input elements is two-way. This means the value is both updated in the view and view-model, which in many cases is great.

As you can imagine, in the case of wanting to format an input element automatically you are instantly fighting with the framework. The problem is the plugin that does the formatting is modifying the input, and Aurelia itself is also modifying the input.

Why not a value converter?

You can create a value converter (my first attempt) and leverage the toView and fromView methods for mutating the values going to and from the view.

A value converter gets you quite a lot of the way, but one problem you will encounter is the caret position will jump to the end. When the value is modified in the input, the entire input is effectively refreshed and the cursor jumps to the end of the value which is jarring and annoying.

How about a custom attribute?

My second attempt involved using a custom attribute that listened to the input and blur events. I added in some checks of the value and attempting to work around the caret position by reading it whenever the input was modified and setting the position after.

Ultimately, I fell into some of the same problems the value converter presented to me. Getting and setting the caret position is tricky business and something I ideally do not want to maintain in the long-term, having to work around issues in different browsers is another problem there.

I knew the only solution had to be leveraging an existing input mask library. A library which supports a plethora of formatting options, masks, working with currencies and other types of data and most importantly: solves the caret jumping problem.

The Solution

I tried a lot of different approaches, referencing implementations for not only Aurelia but Angular and Vue as well. However, the common theme in many of these solutions is they were very complicated. One such plugin I found was over 600 lines of code, many of those specifically for getting and setting the caret.

The final solution ended up being laughably simple, in hindsight. I will show you the code and then run through it below. I am using the inputmask plugin which is a battle-tested and highly configurable input masking plugin.

Whatever library you choose to use, the code will end up looking the same if you follow the approach I have taken.

import { inject, customAttribute, DOM } from 'aurelia-framework';

import Inputmask from 'inputmask';

@customAttribute('input-mask')
@inject(DOM.Element)
export class InputMask {
  private element: HTMLInputElement;

  constructor(element: HTMLInputElement) {
    this.element = element;
  }

  attached() {
    const im = new Inputmask({
      greedy: false,
      alias: 'currency',
      radixPoint: '.',
      groupSeparator: ',',
      digits: 2,
      autoGroup: true,
      rightAlign: false,
      prefix: '$'
    });
    
    im.mask(this.element);
  }
}

export class CleanInputMaskValueConverter {
  fromView(val) {
    if (typeof val === 'string' && val.includes('$')) {
      // Strip out $, _ and , as well as whitespace
      // Then parse it as a floating number to account for decimals
      const parsedValue = parseFloat(val.replace('$', '').replace(/_/g, '').replace(/,/g, '').trim());

      // The number is valid return it
      if (!isNaN(parsedValue)) {
        return parsedValue;
      }
    }

    // Our parsed value was not a valid number, just return the passed in value
    return val;
  }
}

export class PrecisionValueConverter {
  toView(val, prefix = null) {
    const parsedValue = parseFloat(val);

    if (!isNaN(parsedValue)) {
      if (prefix) {
        return `${prefix}${parsedValue.toFixed(2)}`;
      } else {
        return parsedValue.toFixed(2);
      }
    }

    return val;
  }
}

The solution in its simplest terms is three parts:

  • A custom attribute applied to input elements called input-mask which instantiates the plugin and applies the masking options
  • A value converter which strips away any fragments of the mask; $, _ and ,, trims whitespace and then parses the value using parseFloat
  • A value converter which formats a value passed from a view-model and adds a prefix (if specified) and converts the value to a precision of 2 decimal places

During this exploration phase, I stumbled upon a powerful and awesome feature in Aurelia that I did not even know was possible, multiple value bindings. You will notice below that I have a value.one-time as well as a value.from-view binding on the input.

The reason for this was I wanted to initially set the value and then not worry about syncing it again. This allows data loaded from the server to be passed in (initial values, etc). The value.from-view binding updates our view-model every time the value changes.

<template>
    <input 
        type="text"
        value.one-time="value | precision" 
        value.from-view="value | cleanInputMask"
        input-mask>
</template>

It makes a lot of sense that you can do this, but it’s not a documented feature and initially, I wasn’t 100% confident it would work. I am an experimenter, so the worst-case scenario was it wouldn’t work. This is what I love about Aurelia, it can handle anything that you throw at it, even things you think might not work, but end up working.

Basically, this approach creates an inline binding behaviour where you control the direction of the updating process, which is quite powerful and cool.

A great addition to Aurelia itself could be a binding attribute which offers this functionality (allowing a once to view and always from view mode).

Conclusion & Demo

The end result can be seen here, as you can see we have a nicely formatted as you type input element. This was built for a specific use-case so it is missing configuration options, however, I have created a repository here which will have a plugin-friendly version of this soon.

Leave a Reply

Your email address will not be published. Required fields are marked *