Expanding the jQuery Plugin Development Pattern

Doing research on a recent project, I came across an article by Mike Alsup on a “jQuery Plugin Development Pattern”.
If you haven’t seen it already its worth a trip to learningjquery.com to checkout the implementation of his pattern.

“There are a few requirements that I feel this pattern handles nicely:”

  1. Claim only a single name in the jQuery namespace
  2. Accept an options argument to control plugin behavior
  3. -Provide public access to default plugin settings-
  4. Provide public access to secondary functions (as applicable)
  5. Keep private functions private
  6. Support the Metadata Plugin
  7. Allow for extending the object via callbacks (either hooks or events) as applicable
  8. Scope all selectors with the ‘this’ object

This is a great start to “authoring a jQuery plugin”:http://docs.jquery.com/Plugins/Authoring. However, there are a couple more requirements that I would propse adding in.

  1. Claim only a single name in the jQuery namespace
  2. Accept an options argument to control plugin behavior
  3. -Provide public access to default plugin settings-
  4. Provide public access to secondary functions (as applicable)
  5. Keep private functions private
  6. Support the Metadata Plugin
  7. **Allow for extending the object via callbacks (either hooks or events) as applicable**
  8. **Scope all selectors with the ‘this’ object**

So lets take a look at how we can implement this pattern using a tab plugin.

Claim Only a single name in the jQuery namespace

Setting up a new plugin is pretty simple. First we need to add the plugin name to the jQuery.fn namespace.


;(function($){
})(jQuery);

Notice that We scope the setup of this plugin inside of an anonymous function that executes with the jQuery parameter as $. We do this so that the jQuery namespace can be changed in order to play well with other
javascript frameworks. It also allows us to scope jQuery to $ incase the namespace has already been changed.

Next we need to setup the single namespace for our plugin.


;(function($){
$.fn.tabs = function(tabOptions) {
// support mutltiple elements
if (this.length > 1){
this.each(function() { $(this).tabs(tabOptions) });
return this;
}
}
})(jQuery);

Notice we setup the name of our plugin into the jQuery.fn namespace. Also, we add in a bit of trickery. We need to make sure that if this plugin is called on multiple DOM elements, that it will still function properly.
We do this by looping through each of the elements and calling .carousel() on them. We then return “this” in order to allow chaining of methods.


// support mutltiple elements
if (this.length > 1){
this.each(function() { $(this).tabs(tabOptions) });
return this;
}

Accept an options argument to control plugin behavior

By creating an options argument, we can allow the users to expand/change the default behavior of the plugin. The following code adds in a newOptions parameter and then uses jQuerys
built in $.extend to override the default plugin options.


;(function($){
$.fn.tabs = function(tabOptions) {
// support mutltiple elements
if (this.length > 1){
this.each(function() { $(this).tabs(tabOptions) });
return this;
}

// SETUP private variabls;
var tabs = this;

// setup options
var defaultOptions = {
//default options go here.
};

var options = $.extend({}, defaultOptions, tabOptions);

return this;
}
})(jQuery);


Notice the following code it expands the default options:


var options = $.extend({}, defaultOptions, tabOptions);

Provide public access to secondary functions (as applicable)

We can give access to public functions by attching the functions to the magic variable “this”.


;(function($){
$.fn.tabs = function(tabOptions) {
// support mutltiple elements
if (this.length > 1){
this.each(function() { $(this).tabs(tabOptions) });
return this;
}

// SETUP private variabls;
var tabs = this;

// setup options
var defaultOptions = {
debug : false
};
var options = $.extend({}, defaultOptions, tabOptions);

// PUBLIC functions
this.changeTab = function() {
// change Tab
};

this.getOptions = function() {
return options;
};

return this;
}
})(jQuery);


Keep private functions private

Private functions setup in the way below can only be called from within the scope that they were created. What this means is that there is no outside access to these functions. They can only be called
from within another public methods or another private methods. In order to access this data we would need to setup public methods to call these private methods.


;(function($){
$.fn.tabs = function(tabOptions) {
// support mutltiple elements
if (this.length > 1){
this.each(function() { $(this).tabs(tabOptions) });
return this;
}

// SETUP private variabls;
var tabs = this;

// setup options
var defaultOptions = {
debug : false
};
var options = $.extend({}, defaultOptions, tabOptions);

// SETUP private functions;
var intialize = function() {
return tabs;
};

var findTabs = function() {
// do something .
};

var selectTab = function(element, tabCSS, selectedClass){
// do something .
};

var hooks = function() {
// do something .
};

// PUBLIC functions
this.changeTab = function() {
// change Tab
};

this.getOptions = function() {
return options;
};

return intialize();
}
})(jQuery);


Support the Metadata Plugin

Supporting the Metadata plugin is fairly trivial. We test for the existence of the plugin and then add the data that would be attached to the element.


;(function($){
$.fn.tabs = function(tabOptions) {
// support mutltiple elements
if (this.length > 1){
this.each(function() { $(this).tabs(tabOptions) });
return this;
}

// SETUP private variabls;
var tabs = this;

// setup options
var defaultOptions = {
debug : false
};
var options = $.extend({}, defaultOptions, tabOptions);

// SETUP private functions;
var intialize = function() {
// support MetaData plugin
if ($.meta){
options = $.extend({}, options, this.data());
}

return tabs;
};

var findTabs = function() {
// do something .
};

var selectTab = function(element, tabCSS, selectedClass){
// do something .
};

var hooks = function() {
// do something .
};

// PUBLIC functions
this.changeTab = function() {
// change Tab
};

this.getOptions = function() {
return options;
};

return intialize();
}
})(jQuery);


Allow for extending the object via callbacks (either hooks and/or events)

In order to do this, lets take a more in depth look at the changeTab function.
It sounds complicated, however its a trivial task to setup and allow the moveSlider functionality to be altered by the external options.


// PUBLIC functions
this.changeTab = function() {
// change Tab
};

In order to alter the behavior we need to allow the user to pass in some options.


// setup options
var defaultOptions = {
beforeLoad : function() {
// do something .
},
afterLoad : function() {
// do something .
},
debug : false
};

Now, lets update hooks function to see what needs to be done in order to call the function.


var hooks = function(thisObject, hookList, triggerName){
$(presentation).trigger(triggerName, thisObject);
if ($.isFunction(hookList)){
return hookList.apply(thisObject, []).isOK;
} else if ($.isArray(hookList)) {
var result = {
isOK: true
};
for (var i = 0; i < hookList.length; i++) { if (result.isOK) { result = hookList[i].apply(thisObject, [ result.data ]); } } return result.isOK; }else{ return true; } }

What exactly is this doing? First of all the hooks function takes 3 parameters. thisObject is equal to the object that you want the callback function to have "this" set to,
a method to call back on called hookList and the name of the event to be triggered. The reason we call it list, is that we will check to see if hookList is an array of
functions or just a single function. The following call passes in the tabs object and the onstart callbacks.

Now, lets change the this.changeTab to make use of the beforeLoad and afterLoad events.


this.changeTab = function(element){
if (hooks(tabs, options.beforeLoad, "beforeLoad")){
selectTab(element, options.tabsClass, options.tabsSelected);

var tabOptions = {};
if (options['tab' + tabs.mainTab] != undefined) {
tabOptions = options['tab' + tabs.mainTab];
}

$(options.tabPanels, this).hide();

if (options.afterLoad != undefined) {
hooks(tabs, options.afterLoad, "afterLoad")
} else {
$('.tab' + tabs.mainTab, this).show();
}
}
};

In the hooks method we use the .apply in order to pass "this" as the plugin to the next function. They will have access to all of the public methods that are on that instance of the tabs plugin.

Scope all selectors with the 'this' object

This is actually really simple, but will control the scope of the objects returned by jQuery. Without this, you may clobber another widget on the page or jump outside of the element you are working on.
Lets take another look at the selectTab method and see how we can scope our selectors.


var selectTab = function(element, tabCSS, selectedClass){
var selectedTab = 0;
selectedClass = selectedClass.replace(/^./, "");
$(tabCSS).each(function(){
if (this == element) {
tabs.mainTab = selectedTab;
$(element).addClass(selectedClass);
} else {
$(this).removeClass(selectedClass);
}
selectedTab++;
});
};


On the surface, there is nothing wrong with this. However, what happens if we have two different tab groups on the page? Nothing? How about an extremely large DOM? Would this slow down? Yes, actually it would
and you may or may not be acting on a set of tabs that aren't your own.

So how do we fix this?


var selectTab = function(element, tabCSS, selectedClass){
var selectedTab = 0;
selectedClass = selectedClass.replace(/^./, "");
$(tabCSS, tabs).each(function(){
if (this == element) {
tabs.mainTab = selectedTab;
$(element).addClass(selectedClass);
} else {
$(this).removeClass(selectedClass);
}
selectedTab++;
});
};

Wait, that is the same code? Close, but look closer at the $(tabCSS) selector. Notice, we now use $(tabCSS, tabs). (You could also use $(tabs).find(tabCSS); is faster in most browsers).
This limits the scope of the search for the tabs and it won't clobber any other tab widgets on the page.

25 thoughts on “Expanding the jQuery Plugin Development Pattern”

  1. That is some inspirational stuff. Never knew that opinions could be this varied. Thanks for all the enthusiasm to offer such helpful information here.

  2. Can you show an example of how to call a public function? I would think this should work, but it doesn’t.

    $.fn.tabs.selectTab (‘#tab1’);

    1. Here is the easiest way:

      var tabs = $(“#sometab”).tabs();
      tabs.selectTab(“#tab1”);

      That should give you the ability to use the public functions of the tab plugin.

      1. The following:

        var tabs = $(“#sometab”).tabs();
        tabs.selectTab(“#tab1″);

        is working perfectly when the selector gets only one element.

        But if your selector returns multiple elements, you can’t call the public function.

    1. Sorry for the delay Ryan, Yes. It supports multiple instances that will not conflict.

      You could have multiple sets of tabbed content and it would act independently on each set.

  3. I love this whole example – it is all super clean and easy to follow. The one thing I don’t get is the info regarding the support of the Metadata Plugin. Could you explain why you included that code in your pattern example and what is is demonstrating?

      1. Originally I wrote this pattern in 09. At that time there was a plugin to support things like the following:

        <p data-someOption=”someValue”>

        Rather than having the user use : $(“div#tabs”).tabs({ some list of options});

        They could just use: $(“div#tabs”).tabs();

        And in the constructor use check the DOM element for any of the options:

        i.e. in the constructor:
        for (var option in options){
        options[option] = $(tabs).data(option) ? $(tabs).data(option) : options[option]
        }

        Hope that helps.

  4. Hi! Great read.

    I can’t do this:

    var tabs = $(“div.tab”).tabs(); // note: there are multiple div.tab elements!
    tabs[1].close() // (given there is a public .close() function)

    How can you fix this?

  5. I’d really appreciate a demo, or download of a working copy of this. This is a brilliant tutorial, but the events/callbacks are throwing me for a loop. I’d have a much better time deciphering it if you could provide us with a demo/download of the working copy.

    Thanks again for this tutorial.

  6. While I applaud you for writing a post on a subject you know something about. I find the post itself pretty unusable because the code is not formatted in a readable way. For someone who is new to authoring in jquery. Knowing the hierachy and how things are hooking up through the simple act of formatting the code properly is essential. You might keep that in mind for your next post.

  7. I wasn’t able to call a private function from a public one using the structure above. There was no error but even a simple console.log-command in the private function was not executed. Nothing happend! After searching for some hours I found another approach where a private function is at the top of the plugin: ;(function($){

    // SETUP private functions;
    _findTabs = function() {
    // do something .
    };

    $.fn.tabs = function(tabOptions) {
    ...
    }
    This looks simple but was the only solution for me. I don’t know why it does not work with the structure above (using jQuery 1.7.2).

  8. Thank you!
    I start to understand, and it starts to work.
    But what means
    $(presentation).trigger(triggerName, thisObject);
    presentation is not defined anywhere.
    and You say:
    var tabs = $(“#sometab”).tabs();
    tabs.selectTab(“#tab1″);
    Do You mean
    tabs.changeTab(“#tab1”);
    because selectTab is not public.

    Regards, Oliver

Leave a Reply to Tim Cancel reply

Your email address will not be published. Required fields are marked *