jQuery Plugin Patterns
// Simplest plugin ever
(function($) {
$.myPlugin = function() {};
})(jQuery);
// Two ways to call it
jQuery.myPlugin();
jQuery(function($) {
$.myPlugin();
});
With the realization that jQuery gives you no more than this basic structure to write fat, complex plugins, the youthful sense of exploration that brought you to jQuery quickly evaporates into an empty void of nothingness. You google “jQuery plugin tutorial” and ask yourself, “How is this better than <insert framework here>’s class helpers?” Your head feels strangely pressurized. “And how would I test…” Your brain explodes, Scanners-style, without even an understanding of the question you are about to ask. The very moment your brain begins to disentigrate, you realize that you never were alive to begin with; your existence was simply a predetermined, predictable set of events. The question of God is, and always was, irrelevant. The revelation itself is unimportant and unnoticed. You cease to exist.
OK, so that is the worst case scenario, albeit surprisingly frequent and plausable :). I am going to show you that using pure javascript to write your plugins is not only easier to read and comprehend, but extremely powerful. And then I’m going to show you how to test your plugins using qUnit.
Here is quick guide to the “class” structure I will discuss, for you programmer types:
(function($) {
// Private class variable
var a;
// Private class method
function doSomething() {}
// Public class method
$.myPlugin = function() {
// Private instance variable
var b;
doSomething();
doSomethingElse();
// Private instance method
function doSomethingElse() {}
};
})(jQuery);
Before writing a real world plugin, let’s define a strategy for calling jQuery plugins.
jQuery plugins are usually accessed through a single public method. Often times, with more complex plugins, you will want to specify an action to take (play, pause, or stop, for example). You may also want to pass in any number of options to modify the plugin’s functionality or to specify event callbacks.
Given the requirements, this seems like a pretty versatile way to call a plugin:
$.myPlugin({ option: false })
// or
$.myPlugin('action', { option: false });
The implementation:
(function($) {
var defaults = { option: true };
var options;
$.myPlugin = function(action, new_options) {
if (typeof(action) == 'object') {
new_options = action;
action = 'initialize';
}
setOptions(new_options);
// Do something
// if (action == 'initialize')
return options;
};
function setOptions(new_options) {
options = $.extend({}, defaults, options, new_options);
}
})(jQuery);
The options variable maintains state across multiple myPlugin calls. If myPlugin is called without options, the options variable will equal the defaults variable. If options are specified, they are merged with the default options. If myPlugin is called once again with options, those options will be merged with the previous options, and so on.
Now that you know how to handle basic options, let’s specify an event callback as well:
$.myPlugin({
option: false,
event: function() {}
})
// or
$.myPlugin('action', {
option: false,
event: function() {}
});
The implementation:
(function($) {
var defaults = { option: true };
var events = $({});
var options;
$.myPlugin = function(action, new_options) {
if (typeof(action) == 'object') {
new_options = action;
action = 'initialize';
}
setOptions(new_options);
// Do something
// if (action == 'initialize')
// events.trigger('event');
return options;
};
function setOptions(new_options) {
$.each(new_options, function(event, fn) {
if (typeof(fn) == 'function') {
events.unbind(event);
events.bind(event, fn);
}
});
options = $.extend({}, defaults, options, new_options);
}
})(jQuery);
The setOptions method detects when an option is a function. If so, it binds that event to an empty jQuery object.
Let’s harness the real power of jQuery by allowing access to the plugin via element collections:
// Simplest element-based plugin ever
(function($) {
$.fn.myPlugin = function() {
return this;
};
})(jQuery);
// Two ways to call it
jQuery('#id').myPlugin();
jQuery(function($) {
$('#id').myPlugin();
});
The jQuery object has a special fn object that you can assign functions to. Any function attached to fn will also be available to jQuery element collections.
The new plugin call:
$('#id').myPlugin({ option: false });
// or
$('#id').myPlugin('action', { option: false });
Handling plugin options is a bit different when you involve element collections. Your plugin acts on each individual element, and therefore must maintain state for each element separately.
The implementation:
(function($) {
var defaults = { option: true };
$.fn.myPlugin = function(action, options) {
if (typeof(action) == 'object') {
options = action;
action = 'initialize';
}
this.each(function(el) {
el = $(this);
setOptions(el);
// Do something
// if (action == 'initialize')
});
function setOptions(el) {
options = $.extend({}, defaults, el.data('my_plugin:options'), options);
el.data('my_plugin:options', options);
};
return this;
};
})(jQuery);
Here we use jQuery’s data store to associate options with each individual element.
A plugin call with an event callback:
$('#id').myPlugin({
option: false,
event: function() {}
});
// or
$('#id').myPlugin('action', {
option: false,
event: function() {}
});
The implementation:
(function($) {
var defaults = { option: true };
$.fn.myPlugin = function(action, options) {
if (typeof(action) == 'object') {
new_options = action;
action = 'initialize';
}
this.each(function(el) {
el = $(this);
el.trigger('event');
setOptions(el);
// Do something
// if (action == 'initialize')
});
function setOptions(el) {
$.each(options, function(event, fn) {
if (typeof(fn) == 'function') {
el.unbind(event);
el.bind(event, fn);
}
});
options = $.extend({}, defaults, el.data('my_plugin:options'), options);
el.data('my_plugin:options', options);
};
return this;
};
})(jQuery);
As with options, events are attached to the element instead of maintaining state through a class variable.
Finally, let’s take a look at testing this plugin structure using qUnit. The implementation:
(function($) {
var defaults = { option: false };
$.fn.myPlugin = function(action, options) {
if (typeof(action) == 'object') {
options = action;
action = 'initialize';
}
this.each(function(el) {
el = $(this);
setOptions(el);
// Do something
// if (action == 'initialize')
el.trigger('test_each', {
functions: { setOptions: setOptions },
variables: { defaults: defaults, options: options }
});
});
function setOptions(el) {
options = $.extend({}, defaults, el.data('my_plugin:options'), options);
el.data('my_plugin:options', options);
};
return this;
};
})(jQuery);
Here we use a “test_each” event to provide context for the tests. Events allow you to test private variables and functions, eliminating the need for thinking about accessibility all the time. It also eliminates the need for having “this” everywhere and generally making the javascript ugly.
Here is how we might test the last example:
jQuery(function($) {
var functions, variables;
function setup() {
var accessor = $('#id').myPlugin({
option: true,
test_each: function(event, accessor) {
functions = accessor.functions;
variables = accessor.variables;
}
});
}
module('setOptions', { setup: setup });
test('should extend the default options if options specified', function() {
equals(variables.options.option, true);
});
test('should assign default options if no options specified', function() {
$('#id').data('my_plugin:options', null).myPlugin();
equals($('#id').data('my_plugin:options'), variables.defaults);
});
test('should store options in the data store', function() {
equals($('#id').data('my_plugin:options'), variables.options);
});
});
And finally, the HTML to execute the test:
<html>
<head>
<link href="jquery.testsuite" media="print" rel="Stylesheet" type="text/css" />
<script type="text/javascript" src="jquery-1.3.2"></script>
<script type="text/javascript" src="jquery.testrunner"></script>
<script type="text/javascript" src="jquery.my_plugin"></script>
<script type="text/javascript" src="jquery.my_plugin.qunit"></script>
<title>jquery.my_plugin</title>
</head>
<body>
<h1>jquery.my_plugin</h1>
<h2 id="banner"></h2>
<h2 id="userAgent"></h2>
<ol id="tests"></ol>
<div id="main">
<div id="id"></div>
</div>
</body>
</html>
Learn more about qUnit here.
Personally, this way of doing things has reinvigorated my curiosity towards jQuery. Most of all, it has dispelled my worries about jQuery only being a tool for designers. Let me know if you guys find an even cleaner way to execute and test complex objects.