FormatTextBox jQuery Plugin
jQuery plugin that enforces a format on a text box, using jQuery Boilerplate plugin template
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.
// defaults
$("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.
// defaults
$("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: ","
}
}
});
// the text in text box will be "1234,567"
// because the decimal symbol is ","
// parse number: float number 1234.567
$("input:text").formatTextBox("parseNumber");
// format number: string "1.234,567"
$("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.
// defaults
$("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.
// limit date to 2014
$("input:text").formatTextBox({
formatType: "date",
date: {
min: '2014-01-01',
max: '2014-12-31'
}
});
// set date to 01/01/1900 when empty
$("input:text").formatTextBox({
formatType: "date",
valueOnEmpty: new Date('1900-01-01')
});
// set date to today when empty
$("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: "."
}
}
});
// parse date: Date object 31/12/2015
$("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.
// defaults
$("input:text").formatTextBox({
formatType: "time",
time: {
min: null,
max: null
},
nowOnEmpty: false,
valueOnEmpty: null
});
Some examples.
// limit time to 24 hours
$("input:text").formatTextBox({
formatType: "time",
time: {
min: "00:00",
max: "23:59"
}
});
// set time to 12:00 PM when empty
$("input:text").formatTextBox({
formatType: "time",
valueOnEmpty: 1200
});
// set time to now when empty
$("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" });
// parse time: Date object 01/01/1900 23:59:00.
$("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.
// defaults
$("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.
// register Y2K format
$.fn.formatTextBox.namedFormat("Y2K", {
formatType: "integer",
integer: { min: 2000, max: 2999 },
valueOnEmpty: 2000
});
// get Y2K format
$.fn.formatTextBox.namedFormat("Y2K");
// use Y2K format
$("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; // text box 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
});
// clear if the text is 0, otherwise select all the text
$("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; // key pressed
event.preventDefault(); // stop key press
},
onKeypress: function (event, data) {
data.charCode; // key pressed
data.isPrevented; // whether the pressed key was accepted or rejected
}
});
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; // rejected text
},
onBlur: function (event, data) {
},
clearOnError: false,
// or
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) {
/* private plugin members */
var pluginName = "nameOfThePlugin",
// instance of the Plugin object
Plugin = function (elem, options) {
this.elem = elem;
this.$elem = $(elem);
this.options = options;
this._name = pluginName;
this.init();
};
/* Plugin object prototype */
Plugin.prototype = {
defaults: { },
init: function () { },
destroy: function () { }
// plugin functions here
};
Plugin.defaults = Plugin.prototype.defaults;
/* register plugin with jQuery */
$.fn[pluginName] = function (options) {
var args = arguments;
// instantiate a new instance of the plugin
if (options === undefined || typeof options === "object") {
return this.filter("*").each(function () {
if (!$.data(this, "plugin_" + pluginName))
$.data(this, "plugin_" + pluginName, new Plugin(this, options));
});
// call to a public method
} else if (typeof options === "string" && options[0] !== "_" && options !== "init") {
// cache the method call
var returns;
this.filter("*").each(function () {
var instance = $.data(this, "plugin_" + pluginName);
// call the method
if (instance instanceof Plugin && typeof instance[options] === "function")
returns =
instance[options].apply(instance, Array.prototype.slice.call(args, 1));
// allow instances to be destroyed via the "destroy" method
if (options === "destroy")
$.data(this, "plugin_" + pluginName, null);
});
// if the cached method call didn't returned a value
// return this to preserve chainability
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) {
// save the current text
this.currentValue = this.$elem.val();
// clear the text box
if (this.settings.clearOnFocus)
this.$elem.val("");
// select all the text
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) {
// jQuery event.which normalizes event.keyCode and event.charCode
var charCode = event.which;
// if the event was suppressed before, don't do anything
if (event.isDefaultPrevented())
return false;
// keystrokes allowed: control keys, digits, minus sign
if (!(
charCode == 0 || // arrows
charCode == 8 || // backspace
charCode == 9 || // tab
charCode == 13 || // enter
charCode == 27 || // escape
(48 <= charCode && charCode <= 57) || // 0-9
charCode == 45 // -
)) {
event.preventDefault();
return false;
}
// minus sign at the beginning of the text
if (charCode == 45) {
var txtFieldPosition = this._getTextFieldPosition(this.elem);
var caretPosition =
txtFieldPosition.caretPosition - txtFieldPosition.selectionLength;
if (this.elem.value.charAt(0) == '-') {
// already contains -
if (caretPosition != 0) {
event.preventDefault();
return false;
}
} else {
// allow only in the beginning of the text
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;
// IE
if (document.selection) {
elem.focus();
var selection = document.selection.createRange();
selectionLength = selection.text.length;
selection.moveStart('character', -elem.value.length);
caretPosition = selection.text.length;
}
// Firefox
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+$/, // empty, 123, -123
INTEGER_REGEX_2 = /^-?0+$/, // 000, -000
INTEGER_REGEX_3 = /^-?0+[1-9][0-9]*$/; // 000123, -000123
_integerBlur: function (event) {
// the current text
var value = this.$elem.val();
// auto complete
value = this._integerAutoComplete(value, this.settings);
// validation
if (!!value.match(INTEGER_REGEX_1)) {
// empty value
if (value == null || value.toString() == "") {
// specific value when empty, set by the user in the options
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 {
// parse the text to integer
var intValue = this._parseNumber(value, false,
this.settings.integer.format.digitGroupSymbol,
this.settings.integer.format.decimalSymbol
);
// validate that the integer is between min and max values,
// if they are specificed by the user in the options
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); // validation succeeded
else
this._validationError(event, value); // validation error
}
} else {
this._validationError(event, value); // validation error
}
}
// integer auto complete
_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.
// callbacks defined as empty functions
defaults: {
onBeforeFocus: $.noop,
onFocus: $.noop,
onBeforeKeypress: $.noop,
onKeypress: $.noop,
onBeforeBlur: $.noop,
onEmpty: $.noop,
onValidationError: $.noop,
onBlur: $.noop
},
// execute callback
_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);
}
},
// events
_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); }
// focus
_integerFocus: function (event) {
this._onBeforeFocus(event);
// focus code
this._onFocus(event, {
value: this.currentValue
});
},
// keypress
_integerKeypress: function (event) {
var charCode = event.which;
this._onBeforeKeypress(event, {
charCode: charCode
});
// keypress code
this._onKeypress(event, {
charCode: charCode,
isPrevented: false
});
return true;
},
// blur
_integerBlur: function (event) {
this._onBeforeBlur(event);
var value = this.$elem.val();
// if value is empty
this._onEmpty(event);
// if value is not valid
this._validationError(event, value);
this._onBlur(event);
}
History
14/04/2016: Firefox bugfix. Couldn't delete text in Firefox.