Tuesday, November 27, 2012

Guiding User Entry using jQuery Input Masks

Providing masked input entry on a form is an excellent way to guide the user to both enter correct values and require less keystrokes to make those entries. The challenge is determining the boundaries of the functionality the mask provides to achieve various formatted entry. As I worked with the different options available, I quickly found different limits that each library was capable of providing. I've broken down the features I was interested in enabling into three main groups:


  • Masks with fixed formats and no range validation on the user entry other than numeric and/or alpha characters. Phone numbers, tax ID, serial numbers, etc all fall into this group. Its the simplest implementation that is basically only interested in preserving a specific format and forcing certain characters to be entered.

  • Masks with fixed formats with specific range validation on each "field" of the mask. Dates and times are the two most common examples. Each part of the date/time must fall in a certain numerical range to be valid. Dates are further complicated by dependencies on other parts of the date (ie number of days in a month and leap years).

  • Formatted entries that are not really a mask but can use some of the logic provided in the mask library to manage the input. Currency and numbers are the primary concerns in this case. Providing visual cues for thousand separators, decimals, etc. provide helpful feedback to the user when entering larger numbers. You don't want to really show a mask for the entry - you just want the number to format as the user enters the value.



As I evaluated different libraries, I attempted to recreate the functionality outlined above. I started by looking at the options available in the jQuery UI library since I'd like to use masked entry in conjunction with other jQuery UI widgets. At this point, there is no production version of a mask control. On the development wiki, a mask widget is discussed and its basic functionality defined. As I dug through GitHub, I did find it in a separate branch. Given that its clearly not ready for production use, I needed to find a suitable alternative.

I started by trying out Josh Bush's (digitalbush.com/projects/masked-input-plugin) mask plugin. It's functionality is very similar to that described by the Wiki page. One common feature I found both libraries do not have is more advanced control input constraints on the mask. For instance, you can create a mask for dates (99/99/9999) but it does not constrain the range of numbers to only actual dates. Additionally, it does not auto-format the numeric entry with thousand separators, dollar signs, etc.

After some further searching, I found RobinHerbots/jquery.inputmask which actually credits Josh's work as the foundation for the plugin. This more robust implementation has a lot more extensibility. Out-of-the-box, it can understand dates, numeric values, and provides sufficient hooks to add more. One of the features that impressed me was the auto-complete logic for dates and times. If you type 2512 in an input with "m/d/y" specified as the mask, it will properly expand to 02/05/2012. Additionally, the time mask will take 22 hours and convert it to 10 PM. The plugin also has a "decimal" mask that will auto-format numeric entries as they are typed and constrain the values appropriately. When testing the numeric input mask, I had issues with the numericInput option which is suppose to right along the text in the input box. Whenever I'd try to click on the decimal, it would jump to the integer portion of the value. Turning that off, fixed the issue. I also had issues trying to extend the decimal mask to add a dollar sign to the input and integrating it with a jQuery UI spinner. The documentation is a little thin so I may just not be entirely grasping the concept of how to extend it further.

Looking at these different options made me wonder whether there may be an argument to split the physical masking component from the actual validation of the entry. Even though, by definition, the mask is providing a certain degree of validation (only numbers, letters, etc), its main purpose is to provide the formatting so the user is not required to provide it. This is both a short-cut and a method to ensure consistent entry. However, I still believe its important to attempt to provide validation cues to the user as quickly as possible to help guide proper entry. If it can be done while entering parts of a value (like a the month or day in a date), then the user will less likely be required to revisit a field when the final form validation is performed.

As an experiment, I created a demo in my sandbox which uses the digitalbush library to provide the base mask functionality. I extended the library to recognize the different fields of the mask and request validation of each field as the user enters the values into the mask. This functionality allowed me to provide date/time validation similar to the RobinHerbots/jquery.inputmask implementation. The setup of the mask for the date includes the basic mask (99/99/9999) and then a validation function to handle the range checks:


$("#date2")
.mask(
'99/99/9999',
{ validate: function (fld,cur) {
// 0 == month; 1 == day; 2 == year
var mm = parseInt(fld[0]),
dd = parseInt(fld[1]),
yy = parseInt(fld[2]),
vl = true;

if (!(mm >= 0 && mm < 13) && cur == 0) {
fld[0] = '12';
vl = false;
}

if (!(dd >= 0 && dd < 31) && cur == 1) {
fld[1] = '01';
vl = false;
}

if (!(yy >= 1976 && yy < 2199) && cur == 2 && fld[2].replace('_','').length == 4) {
fld[2] = '2012';
vl = false;
}

return vl;
}
});


Its not a perfect implementation - it doesn't handle the dependencies of number days in a month and leap year. It would also make sense to move the validation into a global function in the library (ie $.mask.validate.date) so it could easily be reused and provide different validation for other formats.

For formatted numeric fields, I mainly used the Globalize library to validate and format the entry on each keystroke. I only used the caret() plugin provided in the digitalbush library to help place the cursor in the correct spot in the field as the entry was made:


var old = '';
$("#decimal2")
.on('focus', function (e) {
old = '';
})
.on('keydown', function (e) {
old = $(this).val();
})
.on('keyup', function (e) {
var n = Globalize.parseFloat($(this).val()),
pos;

if (isNaN(n)) {
pos = old.indexOf('.');
$(this).val( old )
.caret( pos, pos);
} else {
n = Globalize.format( n, 'n' );
pos = n.indexOf('.');
$(this).val( n )
.caret( pos, pos);
}
});


Again, a fairly rough implementation but it provides a template for how to approach the problem. The mask plugin already is attached to these events and with some tweaking could be augmented to provide the cursor control but not enforce a fixed mask for the input. Globalize already has a substantial amount of logic to handle numeric parsing/formatting across multiple locales so it makes sense to utilize this functionality in this situation.

Using masked input makes a lot of sense to help guide the user to enter correct data with minimal effort. While there is some work required to achieve certain features, the foundation exists to build these components. It might be awhile before the jQuery UI implementation is ready, but there are some good alternatives available that provide similar functionality that you can expect to see in the final jQuery version.