Organize jQuery Widgets with jQuery.Controller

17 June 2010 by justinbmeyer

Do you like organized code and hate nested functions that are impossible to reuse? Want to be able to extend plugins and widgets? Do you want these widgets to clean themselves up after they are removed? Great! JavaScriptMVC-extracted jQuery.Controller is the easiest and most robust way to build jQuery widgets.

Features

  • Extendable widgets.
  • Deterministic and debug-able code.
  • Auto cleanup.
  • Namespaced widgets.

Download

jquery.controller.js (minimized 7.8kb) [includes jquery.class.js, jquery.event.destroyed.js]

Demos

  1. Tabs widget.
  2. Extending Tabs to use history.

Documentation

JavaScriptMVC’s controller docs.

Use

Using controller is easy. Extend $.Controller with the name and methods of your widget. Methods that look like “event{style=”font-size: 11px; “}” or “selector event{style=”font-size: 11px; “}” are bound (or delegated) when a new controller is created. The init{style=”font-size: 11px; “} method is called after the events are bound.

The following creates a tab widget.

~~~~ {style=”font-size: 11px; “} // 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(); ~~~~

There are a few important things to notice:

  • Creating a Controller class creates a jQuery plugin with the underscored class name. In this case, it created the tabs plugin: $("#tabs").tabs();{style=”font-size: 11px; “}
  • The tabs(){style=”font-size: 11px; “} plugin creates a new Tabs instance on each element matched by the selector. The tabs instance is created with the element and any parameters passed to tabs(){style=”font-size: 11px; “}. In this case it is called likenew Tabs(el){style=”font-size: 11px; “}.
  • When an instance of Controller is created, events are bound, this.element{style=”font-size: 11px; “} is set to the Controller’s element, and init{style=”font-size: 11px; “} is called to setup the widget.

Extending Widgets

Many jQuery widgets suffer from an “everything-but-the-kitchen-sink” mentality. They pack every conceivable type of related functionality. This causes software bloat and slower load times for users. The root problem is the lack of a standard way to organize and extend widgets.

Controller fixes this problem by using Class. The following extends the Tabs widget above to make history enabled tabs.*

~~~~ {style=”font-size: 11px; “} //inherit from tabs Tabs.extend(“HistoryTabs”,{ // ignore clicks “li click” : function(){},

// listen for hashchanges “hashchange” : function(){ var hash = window.location.hash; this.activate(hash === ‘’ || hash === ‘#’ ? this.element.find(“li:first”) : this.element.find(“a[href=”+hash+”]”).parent() ) } })

// create a history tabs $(“#historytab”).history_tabs() ~~~~

*This assumes you are using something like Ben Alman’s jQuery Hashchange Event Plugin.

By using Controller, we are able to keep functionality bloat to a minimum while providing a base class for future complex functionality.

Deterministic Code

Have you ever tried understanding someone else’s jQuery code? You’ll probably see a nested mess of anonymous callback functions wrapped in a self calling anonymous function. Despite there being as many jQuery widgets as there are atoms in the universe, there isn’t a great way to organize jQuery functionality.

If you’re working on a big project, or with other developers, unorganized code can create a big problem real fast. You need code that is deterministic - if you see functionality expressed in the page, you know where that functionality is expressed in code.

Controller provides this by organizing all entry points to a widget (methods and events) and adding the controller’s name to the element’s className. We’ll break these down in the next two sub-sections; but the result is if something happens on the page, say an “li{style=”font-size: 11px; “}” in a “.tabs{style=”font-size: 11px; “}” element is clicked, you know to look for “li click{style=”font-size: 11px; “}” in a Tabs controller.

Organized Methods and Events

Controllers’ prototype properties organize public methods and event handlers. This makes finding ‘where stuff gets happens’ in a widget really easy. For example, if you wanted to activate the last tab in a Tabs, you could call:

~~~~ {style=”font-size: 11px; “} $(‘#tabs’).tabs(“activate”, $(“#tabs li:last”)) ~~~~

But the REALLY clever thing about controller is how it organizes event handlers. By naming a function like “event{style=”font-size: 11px; “}” or “selector event{style=”font-size: 11px; “}”, Controller knows to listen for those events. Event handlers are self labeling! We typically refer to these methods as actions.

Controller even lets you parameterize actions, making it possible to configure the event or selector when the controller is created. And, with the jquery.controller.subscribe.js plugin, you can listen to OpenAjax messages. The following are example actions:

~~~~ {style=”font-size: 11px; “} // listens for all clicks in this controller click : function(el, ev){…}

// changes in .foo .bar element in this controller “.foo .bar change” : function(el, ev) {…}

// listen to this.options.activateEvent events in an li “li {activateEvent}” : function(){…}

//listen for OpenAjax hub “recipe.created” messages “recipe.created subscribe” : function(called, recipe){…}

//listens for drag events with jquery.event.drag.js “.drag dragover” : function(el, ev, drag, drop){ … } ~~~~

Controller Element Label

Controllers label their element’s className{style=”font-size: 11px; “} with their name. For example, after a HistoryTabs controller is added to the #historytab{style=”font-size: 11px; “} element, it would look like:

~~~~ {style=”font-size: 11px; “} <div id='historytab' class='history_tabs'></div> ~~~~

You can also see all controllers on an element with $("#foo").data('controllers'){style=”font-size: 11px; “}.

In summary, Controller organizes functionality so it is easy to find. It provides a flexible but orderly way to organize widgets.

Auto-magic Cleanup

If you’re building a JavaScript application, removing a widget from the page is almost as important as adding one. Unfortunately, the vast majority of jQuery plugins are designed to enhance a static page and this important feature is ignored. Typically, the only way to remove a widget is to remove its element. Even when the element is removed, the widget is often still bound to other objects such as the document which causes errors.

Controller makes cleaning up widgets easy, and in most cases automatic. It also lets you remove a controller without removing the element.

In the previous section, we showed how controller auto-binds actions. Controller also auto-unbinds those actions when the controller is destroyed. A controller can be destroyed in two ways:

  1. The controller’s element is removed from the page with jQuery manipulators like .remove(), .html().
  2. The controller’s destroy method is called like:

~~~~ {style=”font-size: 11px; “} $(‘.hider:first’).controller().destroy() ~~~~

Controller also provides bind{style=”font-size: 11px; “} and delegate{style=”font-size: 11px; “} methods that allow you to listen to events on elements outside the controller’s element. These event handlers will automatically be removed when the controller is destroyed.

The following controller displays “Click to Hide” and hides itself when the document is clicked.

~~~~ {style=”font-size: 11px; “} $.Controller.extend(“CloseOnClick”,{ init : function(){ // call close on document clicks this.bind(document, ‘click’, ‘close’);

// remove and save old content
this.oldChildren = this.element.children().remove();

// show text to hide
this.element.html("<p>Click to hide</p>")   },   close : function(){
this.element.hide()   },   destroy : function(){
.... //we will fill this in later   } });

$(‘.hider’).close_on_click() ~~~~

When a 'hider'{style=”font-size: 11px; “} element is removed, or destroy{style=”font-size: 11px; “} is called on a CloseOnClick controller, the controller’s event handlers are unbound, removing the document click handler.

If your widget has made any modifications to the element, overwrite the destroy function to set things back to their original state.

The following sets back the original contents of the element:

~~~~ {style=”font-size: 11px; “} destroy : function(){ this.element.children.remove(); this.element.append(this.oldChildren) this.oldChildren = null;

// always call super to call base destroy
this._super()   } ~~~~

Namespaces

It’s important to keep plugins from stomping on each other. If you create a Controller with a namespace, it includes the namespace in the jQuery plugin. For example:

~~~~ {style=”font-size: 11px; “} $.Controller.extend(“Bitovi.Tabs”);

// creates –>

$(“#tabs”).jupiter_tabs() ~~~~

This isn’t exactly namespacing and not 100% protection against collision. But in practice, we’ve not had any problems.

Conclusions

Controller is JavaScriptMVC’s, and by extension, Bitovi’s secret-sauce. It helps us build functionality in a robust and repeatable way. It’s hard to express the amazing feeling walking onto a project with 50 controllers I have never seen and instantly start contributing and fixing bugs.

Controller also helps us develop faster because we don’t have to invent a new pattern for every widget.

Controller is not for all jQuery plugins. If you are building something very small, unlikely to be extended, or DOM related, controller is probably not the best fit. But if you find yourself needing to organize your team’s jQuery code around a proven pattern, take Controller for a spin.