<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Bitovi Blog - UX and UI design, JavaScript and Front-end development
Loading

Bitovi |

Writing the Perfect jQuery Plugin

Writing the Perfect jQuery Plugin

Justin Meyer

Justin Meyer

Twitter Reddit

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:

  1. Raise the bar for what's considered a "quality" widget.
  2. 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:

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.

Learn Javascript with Bitovi. We offer advanced training at an affordable price.
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?

 

Bitovi can build your app. See your project done right from start to finish.
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.