Deep Dive
Fancier Forms is completely extensible/overridable. This is because ≥ v1.1.0 of the plugin was written using the Prototype design pattern with those qualities in mind.
The Prototype Pattern
While writing v1.0.0 of this plugin, I noticed that the three form elements this plugin supports—selects, radios, and checkboxes—share a lot of functionality in common, especially radios and checkboxes. Coming from a background in object oriented programming, this translates into an abstract base class with multiple derived classes inheriting from it. After some research and experimentation, I found that the Prototype pattern is a great candidate for achieving this functionality in JavaScript.
Here's the base class I created that represents the core functionality provided by Fancier Forms:
// constructor
var FancyBase = function (element) {
// save a reference to the native element
this.el = element;
};
// function definitions
FancyBase.prototype = {
init: function () {
// kicks everything off
},
fancify: function () {
// creates and saves a reference to the fancy element
},
isChecked: function () {
// determines whether the element is checked
// this doesn't really apply to selects
// (the base class is tailored specifically for checkboxes)
},
isDisabled: function () {
// determines whether the element is disabled
},
focus: function () {
// puts the fancy element in the focused state
},
blur: function () {
// takes the fancy element out of the focused state
},
syncChange: function () {
// updates the fancy element
// whenever a change in the native element occurs
},
changeValue: function () {
// updates the native element
// whenever a change in the fancy element occurs
},
bindEvents: function () {
// convenience function that calls nativeEvents and fancyEvents
},
nativeEvents: function () {
// binds event handlers to the native element
},
fancyEvents: function () {
// binds event handlers to the fancy element
}
};
I know the code block above is fairly long, but hopefully it's not too daunting. A lot of the bulk is due to the comments describing each function's purpose, but it basically just consists of a variable definition and that variable's prototype definition. Let's break it down!
Constructor
var FancyBase = function (element) {
// save a reference to the native element
this.el = element;
};
This creates a new function and stores it in a variable, FancyBase. The convention of starting the variable name with a capital letter instead of a lowercase one signifies that we'll be creating objects from it later using the new keyword. Inside the function, you can declare and initialize local variables, similar to a constructor in object oriented languages. Be sure to precede your variable declarations with this.—that's how you'll retain a reference to them for later access.
Function Definitions
FancyBase.prototype = {
init: function () {
this.fancify();
this.el.addClass("hidden").after(this.fel);
this.bindEvents();
},
...
};
Here, I'm defining FancyBase's prototype with an object literal containing function definitions. Because these functions are defined on the prototype property, they exist only one time in memory and are shared by all FancyBase objects that get created. Due to the prototype chain, other objects inheriting from FancyBase will receive these function definitions as well.
Some JavaScript design patterns, such as the Revealing Module or Revealing Prototype patterns, allow the creation of public and private variables and functions. In contrast, the Prototype pattern supports only public members. But this is actually necessary, given the desired plugin architecture, because public access is required to override function behavior in derived objects. This is the mechanism by which Fancier Forms allows you, the plugin user, to extend/override/customize its functionality however you see fit.
A Note About this
In the Prototype pattern, the this keyword always represents the instantiated object—in this case, FancyBase. Any variable references or function calls must be preceded with this. to work. If you look at the source code, you'll notice in some cases I am capturing this in a local variable called _this.
var _this = this;
Occasionally, this practice is necessary to make the instantiated object available inside closures, such as event handlers. More often than not, however, I am employing that technique to allow JavaScript minifiers/obfuscators to be more effective; most minification gains come from swapping out local variable names with shorter ones.
Instantiation
In the object oriented model I'm trying to emulate, FancyBase would actually be an abstract base class—meaning you can't create objects from its definition. Instead, you would create derived classes and instantiate those. That is the intent for this plugin, but there's nothing in JavaScript to stop us from creating instances of FancyBase, so I'll use it for the example.
var base = new FancyBase();
Now we can call any of the variables and functions we defined on the this keyword.
base.init();
Inheritance
Fancier Forms uses FancyBase to defined the majority of the functionality required by the plugin. But obviously, checkboxes, radios, and selects all differ from each other in various ways. To handle this, the plugin also creates constructs to represent each of those (FancyCheckbox, FancyRadio, and FancySelect) that inherit functionality from FancyBase. These derived objects specify alternate function definitions that override the functionality provided by FancyBase.
The following code snippet adds a new function to the jQuery object called inherit. It automates setting up the prototypal inheritance relationship between two objects, and allows the specification of new and alternate function definitions to add to the derived object.
$.extend({
inherit: function (derived, base, derivedExtrasAndOverrides) {
derived.prototype = new base();
derived.prototype.constructor = derived;
$.extend(derived.prototype, derivedExtrasAndOverrides);
}
});
I learned the principles used here from this blog post. I definitely recommend giving it a read through if you're interested in understanding what's happening. One important difference between my solution and his is that he recommends doing this:
derived.prototype = Object.create(base.prototype);
The issue with that is Object.create is a newer addition to the JavaScript standard, meaning it lacks support in older browsers.
derived.prototype = new base();
Using the above line muddies up the derived objects a little, but gets the job done in older browsers too!
Employing the inherit method to create the FancyRadio looks like this:
$.inherit(FancyRadio, FancyBase, {
// define a new function that's not present on FancyBase
getGroup: function () {
// returns a jQuery object containing all the radio buttons
// in the same group as the current one
},
syncChange: function () {
// override version provided by FancyBase
},
changeValue: function () {
// override version provided by FancyBase
}
});
Extensibility
Fancier Forms defines another function on the jQuery object, called fancyOverrides. It provides the mechanism by which the plugin functionality can be customized without having to alter any of the plugin code, itself. Here's an example of how you might use it:
$.fancyOverrides({
FancyCheckbox: {
init: function () {
console.log("FancyCheckbox init function overridden");
}
},
FancyRadio: {
init: function () {
this.test();
},
test: function () {
console.log("FancyRadio test function introduced");
}
},
FancySelect: {
// change what CSS class characterizes the select as open
open: function () {
this.fel.addClass("dd-open");
},
close: function () {
this.fel.removeClass("dd-open");
}
}
});
And here's its code definition:
$.extend({
fancyOverrides: function (overrides) {
for (var type in overrides) {
switch (type) {
case "FancyCheckbox":
$.extend(FancyCheckbox.prototype, overrides[type]);
break;
case "FancyRadio":
$.extend(FancyRadio.prototype, overrides[type]);
break;
case "FancySelect":
$.extend(FancySelect.prototype, overrides[type]);
break;
}
}
}
});
The fancyOverrides function expects an object literal containing named objects—any combination of FancyCheckbox, FancyRadio, or FancySelect—that have any number of function definitions. It will then loop through each Fancy type and copy the functions to that object. This process should be completed prior to executing the fancify functions, otherwise Fancier Forms will operate using the original Fancy definitions.
$(function() {
// perform customizations
$.fancyOverrides({
FancySelect: {
...
}
});
// then call the various fancify functions
$("select").fancifySelect();
});