Introduction
Did you ever needed to limit what the user can enter in a text box? Maybe you wanted the user to enter just digits but the text box also accepted letters too? The advantage of limiting what the user can enter is that it cuts down on errors and failed validations but it is by no means a replacement of a complete client and server validations.
Enforcing a format on a text box can be achieved by JavaScript or jQuery solely, but once you need to do it many times for several text boxes, the best solution is to wrapped the implementation in a jQuery plugin. I looked for such jQuery plugin that can handle some of the more frequent formats like integer, float, date & time but couldn't find any or at the least I couldn't find one that suited my needs. There are some plugins that are more particular in their implementation, such as enforcing a specific credit card format, but that's not what I wanted so I decided to write this one myself.
In the first half of this article I will overview the plugin, what it can do and how to use it. A thorough examination of each format with all their various settings are also detailed in the demo HTML file with plenty of examples. In the second half of this article I will show how the jQuery plugin was built using a plugin template from jQuery Boilerplate and how all the pieces of code fit together.
FormatTextBox Plugin
The plugin supports the following formats:
- integer - Integer number.
- float - Decimal number. Format is defined by precision & scale.
- date - Short date (such as 31/12/2015).
- time - Time format (such as 23:59).
- regex - General-purpose format. Enforces a format with the use of regular expressions.
$("input:text").formatTextBox({ formatType: "integer" });
$("input:text").formatTextBox({ formatType: "float" });
$("input:text").formatTextBox({ formatType: "date" });
$("input:text").formatTextBox({ formatType: "time" });
$("input:text").formatTextBox({ formatType: "regex" });
The plugin prevents flickering for characters that are not allowed by the given format. For example if the plugin is enforcing an integer format and the user clicked on a letter character, then that letter will not appear and then disappear (flickering). It will simply not appear in the text box.
For most of the formats, the user can set a default value when the text box is empty. The plugin also keeps track of the last value that was in the text box before the user started to write to it. If the user entered a text that doesn't confirm to the specific format, the plugin will revert to the original text.
The plugin handles shorthand values, meaning if the user entered a partial value it will complete it to confirm with the given format. A few examples: For a float format, a value of ".12" will be completed to "0.123". For a date format, a value of "311215" will be completed to "31/12/2015".
Integer Format
The integer format allows only digits in the text box. If the number range (set by min & max settings) is less than 0, it will allow a single - sign at the beginning of the text. Numbers with leading 0 will be truncated.
$("input:text").formatTextBox({
formatType: "integer",
integer: {
format: {
digitGroupSymbol: ","
},
min: -2147483648,
max: 2147483647
},
valueOnEmpty: null
});
This example limits the range to numbers between -10 and 10. If the text box is empty, it will revert to 1.
$("input:text").formatTextBox({
formatType: "integer",
integer: {
min: -10,
max: 10
},
valueOnEmpty: 1
});
Float Format
The float format allows only digits in the text box and the specified decimal symbol. The numbers that are allowed are determined by the precision and the scale. Precision determines the maximum number of digits and the scale determines how many digits are there after the decimal symbol. If the number range (set by min & max settings) is less than 0, it will allow a single - sign at the beginning of the text. Numbers with leading 0 will be truncated.
$("input:text").formatTextBox({
formatType: "float",
float: {
format: {
precision: 18,
scale: 2,
digitGroupSymbol: ",",
decimalSymbol: "."
},
min: null,
max: null
},
valueOnEmpty: null
});
This example limits the range to numbers between -10.123 and 10.456. A number has 3 digits after the decimal symbol (scale 3). If the text box is empty, it will revert to 0.000.
$("input:text").formatTextBox({
formatType: "float",
float: {
format: {
scale: 3
},
min: -10.123,
max: 10.456
},
valueOnEmpty: 0
});
Parse & Format Numbers
The plugin has two utility functions applicable for integer & float formats. parseNumber
function parses the text in the text box and returns a JavaScript integer/float number. formatNumber
function takes the number in the text box and format it with digit group symbols and decimal symbol. These two functions take into account the digit group symbol and the decimal symbol that were used. They can be very useful especially when the symbols are not the trivial ones. This example has non-trivial symbols.
$("input:text").val(1234.567);
$("input:text").formatTextBox({
formatType: "float",
float: {
format: {
scale: 3,
digitGroupSymbol: ".",
decimalSymbol: ","
}
}
});
$("input:text").formatTextBox("parseNumber");
$("input:text").formatTextBox("formatNumber");
Date Format
The date format enables a short date, such as 31/12/2015. The format allows for digits and the specified separator. The date can be limited between min and max values.
$("input:text").formatTextBox({
formatType: "date",
date: {
format: {
shortDate: "dd/mm/yyyy",
separator: "/"
},
min: null,
max: null
},
nowOnEmpty: false,
valueOnEmpty: null
});
Here's some simple examples. Unless is stated otherwise, the default format is dd/mm/yyyy.
$("input:text").formatTextBox({
formatType: "date",
date: {
min: '2014-01-01',
max: '2014-12-31'
}
});
$("input:text").formatTextBox({
formatType: "date",
valueOnEmpty: new Date('1900-01-01')
});
$("input:text").formatTextBox({
formatType: "date",
nowOnEmpty: true
});
This example uses a different date format mm.dd.yyyy.
$("input:text").formatTextBox({
formatType: "date",
date: {
format: {
shortDate: "mm.dd.yyyy",
separator: "."
}
}
});
Parse Date
parseDate is a utility function applicable for date format. This function parses the text in the text box and returns a JavaScript Date object. It takes into account the format of the date and the date separator.
$("input:text").val("12.31.2015");
$("input:text").formatTextBox({
formatType: "date",
date: {
format: {
shortDate: "mm.dd.yyyy",
separator: "."
}
}
});
$("input:text").formatTextBox("parseDate");
Time Format
The time format enables a short time format without seconds, such as 23:59. It is not limited to a 24-hours cycle. The format allows for digits and : separator. The time can be limited between min and max values.
$("input:text").formatTextBox({
formatType: "time",
time: {
min: null,
max: null
},
nowOnEmpty: false,
valueOnEmpty: null
});
Some examples.
$("input:text").formatTextBox({
formatType: "time",
time: {
min: "00:00",
max: "23:59"
}
});
$("input:text").formatTextBox({
formatType: "time",
valueOnEmpty: 1200
});
$("input:text").formatTextBox({
formatType: "time",
nowOnEmpty: true
});
Parse Time
parseTime
is a utility function applicable for time format. This function parses the text in the text box and returns a JavaScript Date object for 01/01/1900 and adding the hours and minutes to it.
$("input:text").val("23:59");
$("input:text").formatTextBox({ formatType: "time" });
$("input:text").formatTextBox("parseTime");
Regex Format
The regex format is where the user can realize his own formats through regular expressions. regex.pattern
is the text pattern. When set to null, the regex pattern will default to .* to allow all patterns. regex.chars
determines which key strokes are allowed, preferably written in a bracket expression, for example [0-9a-zA-Z]. When set to null, allows all key strokes to pass through. regex.length
is the maximum allowed length of the text. When set to null, is disabled. regex.empty
determines whether the text box can be empty or not. regex.pattern
& regex.chars
can be string or RegExp object.
$("input:text").formatTextBox({
formatType: "regex",
regex: {
pattern: null,
chars: null,
length: null,
empty: true
},
valueOnEmpty: null
});
This example enforces an alphanumeric format which the first character must be a letter.
$("input:text").formatTextBox({
formatType: "regex",
regex: {
pattern: "[a-zA-Z].*",
chars: "[0-9a-zA-Z]"
}
});
This example enforces a strong password. At least 16 characters. Has uppercase letters. Has lowercase letters. Has numbers. Has symbols.
$("input:text").formatTextBox({
formatType: "regex",
regex: {
pattern: /(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[-+_=!@#$%^&*|.,:;~?`'"(){}[\]<>\\/]).{16,}/,
chars: /[0-9a-zA-Z-+_=!@#$%^&*|.,:;~?`'\"(){}[\]<>\\/]/
}
});
Named Formats
The user can register formats with the plugin as a key-value pair where the key is the user-given name of the format and the value is the options object. This mechanism can be leveraged to register all the formats before using them on any number of text boxes. The registration is done by the static function namedFormat
, which is a getter/setter function. namedFormat
is not recursive, registering another named format won't work.
$.fn.formatTextBox.namedFormat("Y2K", {
formatType: "integer",
integer: { min: 2000, max: 2999 },
valueOnEmpty: 2000
});
$.fn.formatTextBox.namedFormat("Y2K");
$("input:text").formatTextBox({ formatType: "Y2K" });
Callbacks
The plugin supports several callbacks that the user can register to. The callbacks, in firing order, are onBeforeFocus
, onFocus
, onBeforeKeypress
, onKeypress
, onBeforeBlur
, onEmpty
, onValidationError
, onBlur
. The event argument is a jQuery normalized event object. The data argument is the data that was passed as an option when the format was initialized. this
references the input DOM control.
Focus Callbacks
onBeforeFocus
and onFocus
callbacks fire before and after the plugin handles the focus event. This is when the plugin essentially setting aside the current text before the user starts to write in the text box. onBeforeFocus
is where the user can change the input text before it is processed. onFocus
data argument holds the input text when the focus event occurs. This data argument is useful when options.clearOnFocus = true
because the input is empty at the end of the focus event and the user can't get it through $.val()
.
$("input:text").formatTextBox({
onBeforeFocus: function (event, data) {
},
onFocus: function (event, data) {
data.value;
}
});
The plugin provides some options for common actions on focus event, so the user won't need to implement them with the focus callbacks. clearOnFocus
will clear the text once the input control gains focus. selectOnFocus
will select all the text once the input control gains focus. Both options can take a boolean or a function.
$("input:text").formatTextBox({
formatType: "integer",
clearOnFocus: true,
selectOnFocus: true
});
$("input:text").formatTextBox({
formatType: "integer",
clearOnFocus: function(text) {
return (text == "0");
},
selectOnFocus: function(text) {
return (text != "0");
}
});
Keypress Callbacks
onBeforeKeypress
and onKeypress
callbacks fire before and after the plugin processes the pressed key. onBeforeKeypress
is where you can prevent the Keypress event to continue by calling to event.preventDefault()
. data argument holds the char code of the key and whether the pressed key was prevented or accepted. data.charCode
is the same as event.which. data.isPrevented
is the same as calling event.isDefaultPrevented()
.
$("input:text").formatTextBox({
onBeforeKeypress: function (event, data) {
data.charCode;
event.preventDefault();
},
onKeypress: function (event, data) {
data.charCode;
data.isPrevented;
}
});
The plugin provides some predefined keypress char codes that the user can access through $.fn.formatTextBox.keypressCodes
. This examples shows how to prevent a tilde (~) key stroke.
$("input:text").formatTextBox({
onBeforeKeypress: function (event, data) {
if (data.charCode == $.fn.formatTextBox.keypressCodes.Tilde)
event.preventDefault();
}
});
Blur Callbacks
onBeforeBlur
and onBlur
callbacks fire before and after the plugin handles the blur event. This is when the plugin examines the text in the text box and sees if it confirms to its format. If not, it will revert back to the original text. onEmpty
callback fires if the text box is empty. onValidationError
callback fires if the text in the text box doesn't confirm with the format. onValidationError
data argument holds the rejected text.
clearOnError
will clear the text when the text doesn't confirm with the format instead of retrieving the original text. This option can take a boolean or a function.
$("input:text").formatTextBox({
onBeforeBlur: function (event, data) {
},
onEmpty: function (event, data) {
},
onValidationError: function (event, data) {
data.value;
},
onBlur: function (event, data) {
},
clearOnError: false,
clearOnError: function(text) {
return (text != "");
}
});
This example demonstrates how to use onBeforeBlur
callback and data option to leverage an auto-complete feature on an email format. If the email has no host, it will auto-complete it with "gmail.com". The auto-complete is implemented with the onBeforeBlur
callback and not with the onBlur
callback because we want to verify that the text that was auto-completed confirm with the email regex pattern.
$("input:text").formatTextBox({
formatType: "regex",
regex: {
pattern: /([a-zA-Z0-9'_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+/,
keystrokePattern: /[^@]+@?[^@]*/,
chars: "[0-9A-Za-z'-.@_]"
},
data: {
host: "gmail.com"
},
onBeforeBlur: function (event, data) {
if (data != null && data.host != null && data.host.toString() != "") {
var $elem = $(this);
var value = $elem.val();
if (value != "") {
var index = value.lastIndexOf("@");
if (index == -1) {
value += "@" + data.host;
$elem.val(value);
} else if (index == value.length - 1) {
value += data.host;
$elem.val(value);
}
}
}
}
});
jQuery Boilerplate Plugin Template
We start with a template plugin from jQuery Boilerplate. This site has various templates aimed at simple and advanced use cases. It also has widget templates aimed at jQuery UI. These templates represent cumulation of authoring best practices. The intent is to prevent from the user to reinvent the wheel and let him focus on the plugin core and main logic. The template that FormatTextBox plugin uses is Highly Configurable Pattern. On top of this pattern, it also uses Extending jQuery Boilerplate to provide access to public prototype methods and prevent against multiple plugin instantiations. So we start with this template.
; (function ($, window, document, undefined) {
var pluginName = "nameOfThePlugin",
Plugin = function (elem, options) {
this.elem = elem;
this.$elem = $(elem);
this.options = options;
this._name = pluginName;
this.init();
};
Plugin.prototype = {
defaults: { },
init: function () { },
destroy: function () { }
};
Plugin.defaults = Plugin.prototype.defaults;
$.fn[pluginName] = function (options) {
var args = arguments;
if (options === undefined || typeof options === "object") {
return this.filter("*").each(function () {
if (!$.data(this, "plugin_" + pluginName))
$.data(this, "plugin_" + pluginName, new Plugin(this, options));
});
} else if (typeof options === "string" && options[0] !== "_" && options !== "init") {
var returns;
this.filter("*").each(function () {
var instance = $.data(this, "plugin_" + pluginName);
if (instance instanceof Plugin && typeof instance[options] === "function")
returns =
instance[options].apply(instance, Array.prototype.slice.call(args, 1));
if (options === "destroy")
$.data(this, "plugin_" + pluginName, null);
});
return (returns !== undefined ? returns : this);
}
};
})(jQuery, window, document);
When the plugin is called on a DOM object, it first checks if the DOM object is associated with a Plugin object instance (using jQuery $.data()
). If there is already an instance, then the plugin was already called and initialized. In that case nothing is done and there are no multiple instantiations. If it's the first call, the plugin will create a new Plugin object instance and associate it with the DOM object. If we look at the Plugin object constructor, we can see that it keeps references to the DOM object (this.elem
), its jQuery object (this.$elem
) and the options (this.options
) that were passed in the call. The constructor then calls init
for the first and only time and at that, the plugin is instanced and initialized.
If we want to call a plugin public function, the name of the function must come as the first argument and the rest of the arguments are options to that function. The name of the function must not start with an underscore (_) because that's a private function and we can't call init
again. The plugin retrieves the function, which is located in the Plugin instance, and execute it with the options if there are any.
When we call destroy
, first the function is executed like any other public plugin function. This is where we want to remove any event handlers that we attached to the DOM object and make any other clean operations to restore the DOM object back to its original state. Then, the DOM object is disassociated with its Plugin instance. That signifies that the plugin is no longer operating on the DOM object and subsequent calls on this object are possible.
You can also notice that the template has this.filter("*")
clause. The filter is intended to prevent from the plugin to operate on the wrong types of DOM objects. With FormatTextBox, the filter will be this.filter("input:text")
because the plugin only works on text boxes.
Plugin Implementation
init
function is where we hook up the 3 events, focus, keypress & blur, to the text box. destroy
function is where we unhook these events. When we hook the event handler, we use $.proxy()
to make sure that this
is always pointing to the Plugin
object instance.
Plugin.prototype = {
init: function () {
this.settings = $.extend(true, {}, this.defaults, this.options);
if (this.settings.formatType == "integer") {
this.$elem
.on("focus.formatTextBox", $.proxy(this._integerFocus, this))
.on("keypress.formatTextBox", $.proxy(this._integerKeypress, this))
.on("blur.formatTextBox", $.proxy(this._integerBlur, this));
}
else if (this.settings.formatType == "float") { }
else if (this.settings.formatType == "date") { }
else if (this.settings.formatType == "time") { }
else if (this.settings.formatType == "regex") { }
return this;
},
destroy: function () {
this.$elem.off(".formatTextBox");
}
};
Focus
Now we want to put some meat on the plugin. From now on we will concentrate on implementing the integer format, All the functions will be declared in the Plugin.prototype
object. We start with the focus event handler. The purpose of focus is to save the current text aside, before it is being changed, so later it would be possible to revert back to it if necessary. The focus event also takes care of clearing the text box or selecting all the text in it.
_integerFocus: function (event) {
this.currentValue = this.$elem.val();
if (this.settings.clearOnFocus)
this.$elem.val("");
if (this.settings.selectOnFocus)
this.$elem.select();
}
Keypress
The keypress event handler determines whether a keystroke is valid or not. For the integer format we want to allow only digits and minus sign at the beginning of the text. Every other key stroke is not allowed and is prevented.
_integerKeypress: function (event) {
var charCode = event.which;
if (event.isDefaultPrevented())
return false;
if (!(
charCode == 0 ||
charCode == 8 ||
charCode == 9 ||
charCode == 13 ||
charCode == 27 ||
(48 <= charCode && charCode <= 57) ||
charCode == 45
)) {
event.preventDefault();
return false;
}
if (charCode == 45) {
var txtFieldPosition = this._getTextFieldPosition(this.elem);
var caretPosition =
txtFieldPosition.caretPosition - txtFieldPosition.selectionLength;
if (this.elem.value.charAt(0) == '-') {
if (caretPosition != 0) {
event.preventDefault();
return false;
}
} else {
if (caretPosition != 0) {
event.preventDefault();
return false;
}
}
}
return true;
}
getTextFieldPosition
function returns the caret position and the selection length of the text box. It is very useful when you want to allow certain characters at very specific location, such as beginning or end, in the text box.
_getTextFieldPosition: function (elem) {
var caretPosition = 0;
var selectionLength = 0;
if (document.selection) {
elem.focus();
var selection = document.selection.createRange();
selectionLength = selection.text.length;
selection.moveStart('character', -elem.value.length);
caretPosition = selection.text.length;
}
else if (elem.selectionStart || elem.selectionStart == '0') {
caretPosition = elem.selectionEnd;
selectionLength = elem.selectionEnd - elem.selectionStart;
}
return {
caretPosition: caretPosition,
selectionLength: selectionLength
};
}
Blur
The blur event handler determines whether the text is valid and confirm the given format. If not, it will revert back to the previous text that was in the text box. The blur event handler also takes care of auto-complete scenarios. If the user entered partial input, the function will complete the text to confirm to the format. An example of that is when the format is decimal with two 0s after the decimal point and the user entered an integer, the blur event handler will complete it with ".00" at the end of the text. In the case of the integer format, the auto-complete will take care of leading 0s.
var INTEGER_REGEX_1 = /^\d*$|^-\d+$/,
INTEGER_REGEX_2 = /^-?0+$/,
INTEGER_REGEX_3 = /^-?0+[1-9][0-9]*$/;
_integerBlur: function (event) {
var value = this.$elem.val();
value = this._integerAutoComplete(value, this.settings);
if (!!value.match(INTEGER_REGEX_1)) {
if (value == null || value.toString() == "") {
if (this.settings.valueOnEmpty != null) {
var valueOnEmpty =
this._integerAutoComplete(this.settings.valueOnEmpty, this.settings);
if (!!valueOnEmpty.match(INTEGER_REGEX_1))
this.$elem.val(valueOnEmpty);
else
this.$elem.val(value);
} else {
this.$elem.val(value);
}
} else {
var intValue = this._parseNumber(value, false,
this.settings.integer.format.digitGroupSymbol,
this.settings.integer.format.decimalSymbol
);
var inMinRange = (intValue != null &&
(this.settings.integer.min == null || this.settings.integer.min <= intValue));
var inMaxRange = (intValue != null &&
(this.settings.integer.max == null || intValue <= this.settings.integer.max));
if (inMinRange && inMaxRange)
this.$elem.val(value);
else
this._validationError(event, value);
}
} else {
this._validationError(event, value);
}
}
_integerAutoComplete: function (value, settings) {
value = value.toString();
if (!!value.match(INTEGER_REGEX_2)) {
value = "0";
} else if (!!value.match(INTEGER_REGEX_3)) {
value = this._parseNumber(value, false,
settings.integer.format.digitGroupSymbol,
settings.integer.format.decimalSymbol
).toString(10);
}
return value;
}
Callbacks
Now that the 3 events, focus, keypress & blur are implemented, we want to integrate callbacks into the code. The callbacks are defined as empty functions by default. The _onCallback
function is a private function which is responsible for making the actual triggering of the callback function and all the events refer to _onCallback
for its execution.
defaults: {
onBeforeFocus: $.noop,
onFocus: $.noop,
onBeforeKeypress: $.noop,
onKeypress: $.noop,
onBeforeBlur: $.noop,
onEmpty: $.noop,
onValidationError: $.noop,
onBlur: $.noop
},
_onCallback: function (callbackName, event, data) {
if ($.isFunction(this.settings[callbackName]) && event) {
data = $.extend(true, {}, data, this.settings.data || {});
this.settings[callbackName].call(this.elem, event, data);
}
},
_onBeforeFocus: function (event, data) { this._onCallback("onBeforeFocus", event, data); },
_onFocus: function (event, data) { this._onCallback("onFocus", event, data); },
_onBeforeKeypress: function (event, data) { this._onCallback("onBeforeKeypress", event, data); },
_onKeypress: function (event, data) { this._onCallback("onKeypress", event, data); },
_onBeforeBlur: function (event, data) { this._onCallback("onBeforeBlur", event, data); },
_onEmpty: function (event, data) { this._onCallback("onEmpty", event, data); },
_onValidationError: function (event, data) { this._onCallback("onValidationError", event, data); },
_onBlur: function (event, data) { this._onCallback("onBlur", event, data); }
_integerFocus: function (event) {
this._onBeforeFocus(event);
this._onFocus(event, {
value: this.currentValue
});
},
_integerKeypress: function (event) {
var charCode = event.which;
this._onBeforeKeypress(event, {
charCode: charCode
});
this._onKeypress(event, {
charCode: charCode,
isPrevented: false
});
return true;
},
_integerBlur: function (event) {
this._onBeforeBlur(event);
var value = this.$elem.val();
this._onEmpty(event);
this._validationError(event, value);
this._onBlur(event);
}
History
14/04/2016: Firefox bugfix. Couldn't delete text in Firefox.