Expanding the jQuery Plugin Development Pattern
Recently I was tasked with writing a jQuery plugin for a carousel. I am sure at this point you are asking yourself, “Why would you write another one when there are so many out there?”. I agree. However, while there are alot of very good carousel plugins, the requirements meant that I would end up having to edit the code anywyas. And, since this would be my first shot at writing a jQuery plugin from scratch and I figured it would be a good time to learn the process and to document some best practices/design patterns.
Getting Started:
Read through jQuery Authoring Guidelines, this will get you 90% of the way to writing your first plugin.
Patterns:
If you haven’t seen it yet, you need to checkout a post on LearningJquery.com by Mike Alsup. He came up with a good base for a design pattern on writing jQuery plugins:
There are a few requirements that I feel this pattern handles nicely:
- Claim only a single name in the jQuery namespace
- Accept an options argument to control plugin behavior
- Provide public access to default plugin settings
- Provide public access to secondary functions (as applicable)
- Keep private functions private
- Support the Metadata Plugin
While those are a great start, lets add some new ideas to it.
- Claim only a single name in the jQuery namespace
- Accept an options argument to control plugin behavior
- Provide public access to default plugin settings
- Provide public access to secondary functions (as applicable)
- Keep private functions private
- Support the Metadata Plugin
- Allow for extending the object via callbacks (either hooks or events) as applicable
- scope all selectors with the ‘this’ object
Setting Up the Pattern
For this example, I am going to write a tab plugin. I know, I know… again there are already so many out there. However, we need an example that will show off the 7-9 in the list, and really its not that hard to write.
Here is a full example that meets the first 6 rules in this design. I am not going to go into detail on each of the first 6, since they are well outlined by Mike Alsup.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | (function($){ $.<span class="internal fn">fn.tabs = function(tabOptions) { // support mutltiple elements if (this.length > 1){ this.each(function() { $(this).tabs(tabOptions) }); return this; } // SETUP private variabls; var pOne = ""; var pTwo = ""; //... // SETUP private functions; var findTabs = function() { // do something ... } var hooks = function() { // do something ... } // ... // setup options var defaultOptions = { debug : false }; var options = $.extend({}, defaultOptions, tabOptions); this.intialize = function() { // support MetaData plugin if ($.meta){ options = $.extend({}, options, this.data()); } }; this.changeTab = function() { // change Tab }; this.getOptions = function() { return options; }; return this.intiazlie(); } })(jQuery); |
#7 Extend the plugin via callbacks
Sounds complicated, but this will allow your plugin to remain flexible and not have to be redesigned each time something new and exciting comes up that you need to accomplish. Taking the code above, lets look at first the changeTab() routine.
1 2 3 | this.changeTab = function() { // change Tab }; |
In order to make a call back we need to allow some options to be passed in. We set up the options like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 | var options = { tab0 : { onstart : function() { }, // do something here onload : function() { }, // do something here onfinished : function() { } // do something here }, tab1 : { // lets chain some methods together onstart : function() { }, // do something here onload : [ function() { }, function() { } ], // 2 methods each will be execute onfinished : [ function() { }, function() { } ] // again 2 methods } } |
Now, when each of the two tabs loads, we can check and see if there are any callbacks that need to be called in order to extend the functionality. The new changeTab code would look something like this:
1 2 3 4 5 6 7 8 9 10 11 | this.changeTab = function(element) { // find tab clicked var tabIndex = findTabIndex(element); hooks(this, options['tab' + tabIndex].onstart); // change Tab ... hooks(this, options['tab' + tabIndex].onload); // tab.show(); hooks(this, options['tab' + tabIndex].onfinished); }; |
Now that we are checking to see if the functionality has been extended. Lets look at how we can actually call this in a generic way?
1 2 3 4 5 6 7 8 9 | var hooks = function(thisObject, hookList){ if (<span class="internal typeof">typeof hookList == 'function') { hookList.call(thisObject); } else if (/object|array/.test(<span class="internal typeof">typeof hookList)) { for (var i = 0; i < hookList.length; i++) { hookList[i].call(thisObject); } } } |
What exactly is this doing? First of all the hooks function takes 2 parameters. thisObject is equal to the object that you want the callback function to have “this” set to. and a method to call back on called hookList. 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.
1 | hooks(this, options['tab' + tabIndex].onstart); |
In the hooks method, we have a little magic happening.
1 | hookList.call(thisObject); |
We use the .call(object) method on the function. Basically this sets the “this” inside of the function to the “thisObject” being passed into the function.
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. Inside of the findTabs function we need to find all of the tabs and compare it to the current element that was clicked.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var findTabs = function(element){ var index = 0; var selected = 0; $('.tabs li').each(function() { if (this == element){ selected = index; $(this).addClass('selected'); } else { $(this).removeClass('selected'); } index++; }); return selected; }; |
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 a DOM the size of Amazon.com? 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?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var findTabs = function(element){ var index = 0; var selected = 0; $('.tabs li', this).each(function() { if (this == element){ selected = index; $(this).addClass('selected'); } else { $(this).removeClass('selected'); } index++; }); return selected; }; |
Wait, that is the same code? Close, but look closer at the $(’.tabs li’) selector. Notice, we now use $(’.tabs li’, this). (You could also use this.find(’.tabs li’); which uses less function lookups however thats a topic for another day). This limits the scope of the search for the tabs. It won’t clobber any other tabs on the page and you can even nest tabs once you are using this method.
* Update – Added in new Demo page. You can get the full code to play around with.
Great article, I really like it because I have read the one posted in learningjquery.com
http://mariuzzo.com/
Great article,
Two small things though:
1. You can always use $.isFunction instead of “if (typeof hookList == ‘function‘”.
2. put a semicolon (”;”) before the (function($){, as you might not be able to trust previous scripts on the page.
Cheers
Erik
Umm… I’m sorry to say that but this isn’t quite OK. When you declare this.intialize inside the plugin, it will be available through the jQuery instance as a public method after the plugin is called. But you can’t call it from other jQuery instances, and if you use more than one plugin like this they will break. If you want an object-oriented jQuery plugin you’ll need some more trickery to do that
Actually because of the recursive application in the beginning you may only get access to methods like getOptions through callbacks depending on the number of elements in the original jQuery object. At least you have to wrap the whole thing inside this.each but you still shouldn’t use public methods this way.
Oh, sorry, with only one element it returns undefined.
Just one more thing: inside the initialize function `this` will refer to the window object but even if the scope was right `this.data()` would return a unique ID associated with the first element of the jQuery object.
@Erik
1. I am still getting used to some of the built in functions of jQuery, since the last year was spent mainly in Prototype. I will change it to $.isArray as well rather than testing for /array/.test().
2. Good tip.
I forgot to add in the return this; inside of this.intialize();
/object|array/.test(typeof hookList)
Arrays in javascript have a type: “object”, that’s why there’s an isArray function, which is obviously unnecessary in this specific case.
Actually, when called the way it is, the scope of “this” inside of this.intialize() is the DOM element/jQuery Object and not window. Its because of the way the object is scoped.
try something like this:
Output:
[div#widget]
[div#widget]
Nope, put a console.log(this) inside the this.initialize function’s body! This is very unintuitive but unfortunately this how javascript works if you declare methods this way.
Or you can try this:
var a = function(){};
a.prototype.m = function(){
var that = this;
this.method=function(){
console.log(”this: “, this);
console.log(”this == that: “, this == that);
}()
}
new a().m();
@brian
Absolutely. That would be the correct behavior. Notice in the following code how the this.intialize function is being called.
OUTPUT:
this: Window expanding-the-jquery-plugin-development-pattern
this == that false
OUTPUT:
this: Object
this == that true
On a side note, I think I just found a good topic for the next post!
JC
Really nice put together in a nutshell.
Sorry, if this is a rather dumb question, but i couldn’t get the “secondary functions” not to work. How are they supposed to be used? I tried with a simple example and defined the method whith this.something=function().
But when i try to use my “secondary function” i only get an “not a function” error.
$(’.test’).myPlugin();
// Try to call my secondary method:
$(’.test’).myPlugin.something();
Did you try $(’test’).myPlugin().something()?
If you pass options to myPlugin you will need to call it like this:
$(’test’).myPlugin(options).something().
Also you can do something like this:
var myPlugin = $(’test’).myPlugin();
myPlugin.something();
Hope that helps.
JC
Hi jcfant,
Concerning your code to manage multiple elements, I never seen that trick before, it seems really too easy.. is there disadvantages?
Maybe problems of slow with big plugins?
Thanks
It depends on what you mean by Big plugins. Assuming you aren’t calling $(”BODY”).myplugin(); then the speed so far hasn’t been an issue. Without going into too many specifics this is being done on some of the sites I work on at work, and has performed flawlessly for a while.
I always try to execute the plugins as close to the code I want to manipulate as possible. For example, if I am setting up tabs, I would use something like: $(”.tabContainer”).tabs() rather than having the tabs plugin find anything that could be used as tabs.
Hope That helps, if not feel free to contact me jcfant at gmail.com
The syntax for declaring public methods you use actually declares them as global jQuery methods. For example your function:
this.changeTab = function () {…};
Ends up being accessible through
$(’#example’).changeTab();
This claims an additional name in the jQuery namespace for each public method declared.
To add methods under the plugin namespace you need to declare them something like this:
$.fn.tabs.changeTab = function () {…};
Ryan, actually the way the functions are declared won’t be in the global jQuery namespace. The function is declared inside of the scope of the $.fn.tabs function.
Note, that the this.changeTab = function() { … } is inside of $.fn.tabs = function(tabOptions) { }.
it ends up looking like:
Try opening firebug and running this:
note the output:
PLUGIN CREATION [body]
FUNCTION SCOPE [body]
TypeError: $(”BODY”).testFunction is not a function
I am a little confused. I am trying to get access to the variables and functions within this.each( function() { but even after reading all this and trying everything still have a lot of difficulty…
Any feedback will be greatly appreciated.
(function($){
//bunch of initialization variables and functions
$.fn.myPlugin = function(opts) {
opts = $.extend({}, $.fn.myPlugin.defaults, opts);
return this.each( function() {
//doing bunch of stuff with $(this)
});
};
$.fn.myPlugin.defaults = {
// bunch of default options
};
})(jQuery);
});
Mike, I think the problem lies in how “this” is handled in Javascript.
inside of the following:
$(this) would refer to the DOM element that the function is called on.
You could access any of the variables inside of $.fn.myPlugin by calling them directly:
alternatively you may want setup a variable that you can access from within the each that has the information you need.
Hope that helps.
JC
Thanks JC. This would solve my problem for variables. But is there anyway that I can trigger a function inside return this.each( function () {. Basically I have a plugin working on a DOM element but I want to be able to change its behavior based on a behavior of another DOM variable (from within another jquery plugin). I am wondering if I have to update my variables and reset this plugin or can I somehow call the functions within plugin from another plugin.
@Mike
It truly depends on how you scope your functions.
notice the two different ways of creating functions. One is able to be accessed from any reference to the plugin and one is scoped to be hidden.
Because getFalse is scoped to only be available inside of the function you cannot call it from any where other than in your plugin.
From another plugin you could then access getTrue() but not getFalse.
Hope this helps.
Thanks again for your prompt response. Let me ask it this way is there any way that I can access $(this) inside this.each( function() { … inside the getTrue or getFalse functions that you have defined.
@Mike
Inside of getFalse you would have to use “self” to access “this”. inside of getTrue, yes you should be able to access “this”.
I am really sorry but it seems like I have failed to communicate my problem for 3 times now. How can I potentially do something with $(this) which is the DOM element inside the getTrue or getFalse functions unless I can somehow pass it to them? It would be great if you could give me a sample. Again I am not talking about the jquery object this but the $(this) DOM object inside the this.each{ function() …
Thanks a lot for all your help
@Mike
Hopefully I understand you correctly:
You can actually run this in firebug on this page and it will produce the following results.
<ol>
TRUE <ol> [ol]
FALSE: <ol> [ol]
Maybe if you pasted some code that isn’t working I could help walk you through it.
Great, I think we are almost there. So, my question is now: how can I call the getTrue function from outside of the plugin which needs the DOM object to be passed to the function. In other words what would I need to put inside $(’BODY’).myPlugin().getTrue( ??? );
I cant thank you enough for putting up with me.
Sorry but could you please let me know if this is possible at all or I will have to use some other way to do this. Many thanks in advance.