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 usingparseFloat
- 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.
Thanks for your all of your posts. I always enjoy reading your explorations of Aurelia.
During reading this post though I did not like the solution because if the value were to change in the viewmodel it would not be displayed on the view due to the one-time binding. I played with your sandbox to try a slightly different approach. Did you try to do the normal bind with an updateTrigger:’blur’? If you do this then at least in my limited testing it seems to work (the mask behaves as expected) and if the value is updated by an event or some other action then it will be synchronized. I am not sure why you applied the precision value converter since the mask takes care of the view display, but you need to apply the fromView to clear out the formats. My final binding looked like this:
Perhaps there is a side effect that I am not taking into account and if so I would love to hear what it is.
thanks again.