jQuery is an excellent DOM abstraction, but compared to other libraries, it leaves much to desire towards building complex, reusable functionality. There's lots of good examples and resources, but most of them fall short in providing a pattern that is:
- extensible - you can add new features or derive new functionality from old functionality.
- Organized - the plugin is structured without a lot of anonymous functions
- Destroyable - you can remove the plugin without lingering side effects
- Deterministic - find out what's going on where.
Publication's Purpose
My goal for this article is two-fold:
- Raise the bar for what's considered a "quality" widget.
- Demonstrate a simple 'perfect' plugin pattern.
You can find all the source code and examples in this JSFiddle. But before we get into it, some throat clearing ...
Perfect Perspective
The perfect plugin is many things. Unfortunately, it would take me months to cover important techniques such as:
- Progressive enhancement
- Event oriented architecture
- High performance plugins
Futhermore, I'm focusing on a very specific type of plugin - widget plugins. Widget plugins listen to events and change the behavior of the page.
Fortunately, this covers a lot of what beginner jQuery-ers want to build and the plugins people share. Tabs, grids, tree, tooltips, contextmenus are all widget plugins.
Prior Patterns
There's a lot of other widget patterns. The following are some solid low-level writeups:
- Using inheritance patterns to organize Large jQuery Applications by Alex Sexton
- Using Objects to Organize Your Code by Rebecca Murphey
We'll extend on these ideas to add some important features.
Finally, there are tools that do provide these characteristics:
I will write about these at the end of the article and why you should use them.
Problem Plugins
There are serious problem with how many people build jQuery widgets. The following could be a first cut of a context menu that shows content in an element and removes it when someone clicks away.
$.fn.contextMenu = function(content){
var el = this[0];
setTimeout(function(){
$(document).click(function(ev){
if(ev.target != el){
$(el).hide()
}
}
},10)
$(el).html(content).show();
})
This code might be fine for now, but what if you wanted to:
- Remove the plugin? How are you going to remove the event listener on the document? It's going to keep
el
in memory until the page refreshes. - Change show and hide to fade in and out.
- If you saw this happening, how could you find this code?
The Perfect Pattern
To illustrate the perfect-plugin-pattern, I'll use a basic tabs widget that we'll extend to a history tabs. Lets start with what a first attempt at a Tabs might be:
(function() {
// returns the tab content for a tab
var tab = function(li) {
return $(li.find("a").attr("href"))
},
// deactivate the old active tab, mark the li as active
activate = function(li) {
tab(li.siblings('.active')
.removeClass('active')).hide()
tab(li.addClass('active')).show();
},
// activates the tab on click
tabClick = function(ev) {
ev.preventDefault();
activate($(ev.currentTarget))
}
// a simple tab plugin
$.fn.simpleTab = function() {
this.each(function() {
var el = $(this);
el.addClass("tabs").delegate("li", "click",tabClick)
.children("li:gt(0)")
.each(function() {
tab($(this)).hide();
});
activate(el.children("li:first"));
})
}
})();
I'm using as an example a simple tabs that we will extend to a history tabs.
You can see this "Simple Tab" at the top of the example page.
Although we'll add an additional 150 lines it takes to make this the 'perfect' plugin. At the end of this article, I'll show you how to get this back down to 20 lines and still be perfect.
Extensible
We want to make a history enabled tabs with our plugin. So, we should start by making our base tabs widget extendable.
There's a lot of techniques out there for extending JavaScript objects. But, JavaScript provides us a fast and preferred technique - prototypal inheritance.
The first thing we'll do is make a tabs constructor function that we can use. I'll namespace it with my company name so there are no conflicts:
Jptr = {};
Jptr.Tabs = function(el, options) {
if (el) {
this.init(el, options)
}
}
$.extend(Jptr.Tabs.prototype,
{
name: "jptr_tabs",
init: function(el, options) {}
})
Now I'll create a skeleton of the history tabs widget. I'll make the HistoryTabs extend the base Tabs widget.
Jptr.HistoryTabs =
function(el, options) {
if (el) {
this.init(el, options)
}
};
Jptr.HistoryTabs.prototype = new Jptr.Tabs();
$.extend(Jptr.HistoryTabs.prototype, {
name: "jptr_history_tabs"
})
And, I'll use this handy little plugin creator to turn this class into a jQuery plugin:
$.pluginMaker = function(plugin) {
// add the plugin function as a jQuery plugin
$.fn[plugin.prototype.name] = function(options) {
// get the arguments
var args = $.makeArray(arguments),
after = args.slice(1);
return this.each(function() {
// see if we have an instance
var instance = $.data(this, plugin.prototype.name);
if (instance) {
// call a method on the instance
if (typeof options == "string") {
instance[options].apply(instance, after);
} else if (instance.update) {
// call update on the instance
instance.update.apply(instance, args);
}
} else {
// create the plugin
new plugin(this, options);
}
})
};
};
I can use pluginMaker to turn Jptr.Tabs and Jptr.HistoryTabs into jQuery widgets like:
$.pluginMaker(Jptr.Tab);
$.pluginMaker(Jptr.HistoryTabs);
This allows us to add tabs to an element like:
$('#tabs').jptr_tabs()
And call methods on it like:
$('#tabs').jptr_tabs("methodName",param1)
So, we now have two extendable classes, that we've turned into jQuery plugins. Our classes don't do anything yet, but that's ok, we'll take care of that later.
Deterministic
It's great if we know, just by looking at the DOM, which objects are controlling which elements. To help with this, we'll:
- Save a reference to the element on the widget
- Save the plugin instance in the element's data
- Add the name of the widget to the element
Jptr.Tabs init method now looks like:
init : function(el, options){
this.element = $(el);
$.data(el,this.name,this);
this.element.addClass(this.name)
}
This makes it much easier to debug our widget. Just by looking at the html, we can see which widgets are where. If we want more information on the widget, we can just do:
$('element').data('name') //-> widget
To get our widget back.
Finally, if we get a widget, we know where to look to see which element the widget is on (ie, widget.element).
Destroyable
For big apps, it's important to let multiple plugins to operate on the same element or elements. This is especially needed for behavioral or event-oriented plugins.
For this to work, you need to be able to add and remove plugins on the same element without affecting the element or the other plugins.
Unfotunately, most jQuery plugins expect you to remove the element entirely to teardown the plugin. But what if you want to teardown (ie, destroy) the plugin without removing the element?
With most plugins, to teardown the plugin, you simply have to remove its event handlers. So, the tricky part is knowing when to remove the functionality.
You need to be able to remove a plugin both programatically and when the element it operates on is removed.
We'll listen for a 'destroyed' event that happens when an element is removed from the page via the jQuery modifiers: .remove, .html, etc. This will call our teardown method.
We'll also add a destroy function that removes the event handlers and calls teardown.
Our Tabs widget becomes:
$.extend(Jptr.Tabs.prototype, {
init : function(el, options){
// add the class, save the element
this.element = $(el).addClass(this.name);
// listen for destroyed, call teardown
this.element.bind("destroyed",
$.proxy(this.teardown, this));
// call bind to attach events
this.bind();
},
bind: function() { },
destroy: function() {
this.element.unbind("destroyed",
this.teardown);
this.teardown();
},
// set back our element
teardown: function() {
$.removeData(this.element[0],
this.name);
this.element
.removeClass(this.name);
this.unbind();
this.element = null;
},
unbind: function() { }
})
Phew, this is a lot of code, but it's worth it. We made sure that our widgets clean themselves up when their element is removed from the page. Further, we made it so we can remove the widget programatically like:
$('.jptr_tabs').jptr_tabs("destroy")
// or like:
$('.jptr_tabs').data("jptr_tabs").destroy()
Organized
Now we just have to add our functionality back in. Tabs now looks like:
$.extend(Jptr.Tabs.prototype, {
// the name of the plugin
name: "jptr_tabs",
// Sets up the tabs widget
init: function(el, options) {
this.element = $(el).addClass(this.name);
this.element.bind("destroyed",
$.proxy(this.teardown, this));
this.bind();
// activate the first tab
this.activate(this.element.children("li:first"));
// hide other tabs
var tab = this.tab;
this.element.children("li:gt(0)").each(function() {
tab($(this)).hide();
});
},
// bind events to this instance's methods
bind: function() {
this.element.delegate("li", "click",
$.proxy(this.tabClick, this));
},
// call destroy to teardown while leaving the element
destroy: function() {
this.element.unbind("destroyed", this.teardown);
this.teardown();
},
// remove all the functionality of this tabs widget
teardown: function() {
$.removeData(this.element[0], this.name);
this.element.removeClass(this.name + " tabs");
this.unbind();
this.element = null;
var tab = this.tab;
// show all other tabs
this.element.children("li")
.each(function() {
tab($(this)).show()
});
},
unbind: function() {
this.element.undelegate("li","click",this.tabClick)
},
// helper function finds the tab for a given li
tab: function(li) {
return $(li.find("a").attr("href"))
},
// on an li click, activates new tab
tabClick: function(ev) {
ev.preventDefault();
this.activate($(ev.currentTarget))
},
//hides old activate tab, shows new one
activate: function(el) {
this.tab(this.element.find('.active')
.removeClass('active')).hide()
this.tab(el.addClass('active')).show();
}
});
Notice how functions are clearly labeled and are not in anonymous functions! Although longer, this code is much more readable.
Extensible Cont
Finally, we want to make our history tab. The code looks like:
Jptr.HistoryTabs.prototype = new Jptr.Tabs();
$.extend(Jptr.HistoryTabs.prototype, {
name: "jptr_history_tabs",
// listen for hashchange
bind: function() {
$(window).bind("hashchange",
$.proxy(this.hashchange, this));
},
// clean up listening for hashchange.
// this is really important
unbind: function() {
$(window).unbind("hashchange", this.hashchange);
},
// activates the tab represented by the hash
hashchange: function() {
var hash = window.location.hash;
this.activate(hash === '' || hash === '#' ?
this.element.find("li:first") :
this.element.find("a[href=" + hash + "]")
.parent())
}
});
Notice how easy it is to convert a normal tabs to a history enabled tabs. Of course, inheritence isn't necessarily the best pattern, but sometimes it is. The "perfect-plugin-pattern" gives you inheritence by default. Use it or don't. Don't cost nothin.
Also notice how this tabs widget will unbind the window hashchange event handler if the element is removed, or the plugin is destroyed.
Widget Factories
This pattern is VERY similar to jQueryUI's widget and JavaScriptMVC's controller. They both provide extendable, deterministic, destroyable widgets. But controller has one (in our opinion) critical advantage - it will automatically unbind event handlers.
This allows a tab's widget with controller to look like:
// create a new Tabs class
$.Controller.extend("Tabs",{
// initialize code
init : function(el){
// activate the first tab
this.activate( $(el).children("li:first") )
// hide other tabs
var tab = this.tab;
this.element.children("li:gt(0)").each(function(){
tab($(this)).hide()
})
},
// helper function finds the tab for a given li
tab : function(li){
return $(li.find("a").attr("href"))
},
// on an li click, activates new tab
"li click" : function(el, ev){
ev.preventDefault();
this.activate(el)
},
//hides old activate tab, shows new one
activate : function(el){
this.tab(this.find('.active').removeClass('active'))
.hide()
this.tab(el.addClass('active')).show();
}
})
// creates a Tabs on the #tabs element
$("#tabs").tabs();
Controller recognizes function names like "li click" and will automatically unbind them when the controller is destroyed.
Conclusions
I believe in widget factories, and it's dissapointing that they aren't used more in third party jQuery plugins. Our hope is that articles like this can highlight their importance both by showing the necessity of the features they provide and how cumbersome it is to do it yourself.
Regardless of your choice of 'widget factory', it's important think about the characteristics that practically every jQuery widget should have.
If you need the destroyed event, you can find it on jQuery++'s site.
If you're looking for our implementation of this pattern, you can find it in CanJS's can.Control.