Wednesday, August 17, 2011

jQuery Plugin Design Patterns: Part I

jQuery plugins come in all shapes and sizes. They perform very simple or very complex tasks depending on their intended use. When it comes time for you to design your own plugin, its really important to understand what patterns other developers use and what will best suits your needs.
As a side note, I have built a number of plugins for my own projects and client work that will never be released. The jQuery community needs to stop thinking of plugins only as releasable, open-source projects, and start thinking of them instead as a reusable pieces of code that can help optimize and DRY up a complex website. Take this little plugin as an example:
$.fn.notice = function(){
    return this.slideDown(500).delay(4000).slideUp(500);
}
It simply abstracts the animation for displaying a notice (using jQuery 1.4). Would I ever release this on an open source site? Of course not! But it is still a fully functioning jQuery plugin that can be used over and over throughout a website.

Design Pattern Series Overview

  • Basics & Structure (this article)
  • Options & Updating
  • Callbacks & Custom Events
  • Misc. General Practices

Basics

Filename

Every jQuery plugin sits in its own JS file, and is normally named using the following pattern:
jquery.pluginname.js
jquery.pluginname.min.js
Released plugins often also have a version number:
jquery.pluginname-1.3.js
jquery.pluginname-1.3.min.js
If you end up building a lot of plugins for your website, consider also including a namespace to keep all your files together (And of course you would combine and minify them for production, right?):
jquery.mysite.pluginname.js
jquery.mysite.pluginname.js

Basic File Layout

After any comments you choose to put at the top of your file, the very next thing you should have is a self executing anonymous function that will actually wrap your entire plugin. Say what!? Don’t worry, you have seen it before, and it looks like this:
(function($){
 
   ... code here ...
 
})(jQuery);
This gives your plugin a private scope to work in, and also allows your plugin to be used with $.noConflict mode without creating a problem. By passing jQuery into the function, the $ will equal jQuery inside the function even if $ means something different outside your plugin.

Structure

There are three basic structures you will see when you look at released plugins:

Contained Function

In this structure, (almost) all the code to run your plugin is contained within the function used to call your plugin. This is the most common format:
(function($){
 
   $.fn.myPlugin = function(){
      return this.each(function(){
         // do something
      });
   }
 
})(jQuery);
You should use this this structure when you are writing a simple plugin that acts once upon the jQuery result set on each execution. For complex plugins that need to maintain an adjustable state, you should consider the “Class and Function” structure.

Class and Function

In this structure, a class is used and an instance is created for each DOM element in the result set. You may see these objects attached in some way to the DOM elements they modify:
(function($){
   var MyClass = function(el){
       var $el = $(el);
 
       // Attach the instance of this object
       // to the jQuery wrapped DOM node
       $el.data('MyClass', this);
   }
 
   $.fn.myPlugin = function(){
      return this.each(function(){
         (new MyClass(this));
      });
   }
 
})(jQuery);
You should use this structure if you need to be able to access the object later that is associated with a DOM element. It is far easier to attach a single object vs several key/value pairs using the data() method. In this approach, you can access the object by calling $('selector').data('MyClass'). This functions more like a widget and is a plugin that maintains state and can adjust its state on the fly (Learn how in the next article.).
Note: Widget Factory: The next release of jQuery UI is going to see the addition of a Widget Factory that will be designed to specially assist in developing widget-like plugins.

Extend

In my opinion, this is the least idiomatic way to create a jQuery plugin. It uses the jQuery.fn.extend method instead of jQuery.fn.pluginName:
(function($){
 
   $.fn.extend({
     myPlugin: function(){
      return this.each(function(){
         // do something
     },
     myOtherPlugin: function(){
      return this.each(function(){
         // do something
     }
   });
 
})(jQuery);
You will find this structure helpful if you need to add a large number of plugin methods to jQuery. I would contend, however, that if your plugin needs to add that many methods, there may be a better way to structure its implementation.
I feel the extend method is used more by programmers transitioning from other JS libraries. I personally find the simple $.fn.pluginName = structure to be easier to read, and more in line with what you will normally see in jQuery plugins.

Up Next

In the next part of this series, we will look at passing options and providing methods for updating settings and functionality after a plugin has been called.

Jquery Plugin

For some jQuery plugins it would be useful if we could expose one or more functions, so that we can interact with it from Javascript outside the plugin.  If we follow the standard mechanism of plugin authoring, we can only interact with the it at the moment of initialisation.  This post will look at two mechanisms that can be used to expose and access plugin functions.
This post will be based around the following mediaPlayer fictional plugin:
  1. (function($) {  
  2.   $.fn.mediaPlayer = function(options){  
  3.   .........  
  4.   }  
  5. })(jQuery);  
This mediaPlayer plugin would work by being attached to a selected DIV element in the DOM, like this:
  1. $('.myDiv').mediaPlayer(options);  
Now suppose we now want to interact with our mediaPlayer from outside. For example, we may want to trigger the media player to play a certain file. It would be useful if we could have a play() function on our plugin so we can trigger this action. So, how can we do this?

Class and Function Mechanism

With the standard mechanism of building a jQuery plugin we always ensure that the plugin function returns this (the current context of the plugin). The reason for returning the current context is that it allows the plugin call to be chained onto another plugin call. For example:
  1. $('.myDiv')  
  2.    .mediaPlayer(options)  
  3.    .otherPlugin();  
There is another approach to building jQuery plugins, called the class and function approach (see this excellent article of jQuery design patterns). Instead of returning the current context, the jQuery plugin creates and then returns a new instance of a JavaScript class. So, our mediaPlayer plugin could be created in the following way:
  1. (function($) {  
  2.   //the class  
  3.   var player = function(options){  
  4.     return createApi();      
  5.   
  6.     function createApi(){  
  7.       return {  
  8.         play : play  
  9.       }  
  10.     }  
  11.   
  12.     function play(url) {  
  13.       //our play code  
  14.     }  
  15.   }  
  16.   //the function  
  17.   $.fn.mediaPlayer = function(options){  
  18.     //create an instance of the class and return from the plugin  
  19.     return new player(options);  
  20.   }  
  21. })(jQuery);  
If the Javascript that creates the plugin stores a reference to returned instance it can continue accessing it’s public API. For example:
  1. var myMediaPlayer = $('.myDiv').mediaPlayer(options);  
  2. myMediaPlayer.play(url1);  
  3. myMediaPlayer.play(url2);  
However, there is a bit of a problem with this approach. Because it returns the instance of the class rather than the current context, it breaks the chaining of plugins in jQuery. We can no longer do this…
  1. $('.myDiv')  
  2.    .mediaPlayer(options)  
  3.    .otherPlugin();  
…we would have to do this instead:
  1. $('.myDiv')  
  2.    .mediaPlayer(options);  
  3. $('.myDiv')  
  4.    .otherPlugin();  

Execution Through apply() Mechanism

An alternative, and my preferred approach, is to execute the ‘public’ functions in the main plugin function by use of the apply() command. For this approach to work the plugin function has to be effectively overloaded, so that it can be used in two ways. It needs to retain it’s constructive form which in out case takes one argument of options, but it also needs to be able to accept an alternative call that takes a string argument (function name) and any additional arguments that this function requires.
The example below shows how we might use this mechanism to build our mediaPlayer plugin.
  1. (function($) {  
  2.     //define the commands that can be used  
  3.     var commands = {  
  4.         play: play,  
  5.         stop: stop  
  6.     };  
  7.   
  8.     $.fn.mediaPlayer = function() {  
  9.         if (typeof arguments[0] === 'string') {  
  10.             //execute string comand on mediaPlayer  
  11.             var property = arguments[1];  
  12.             //remove the command name from the arguments  
  13.             var args = Array.prototype.slice.call(arguments);  
  14.             args.splice(0, 1);  
  15.   
  16.             commands[arguments[0]].apply(this, args);  
  17.         }  
  18.         else {  
  19.             //create mediaPlayer  
  20.             createMediaPlayer.apply(this, arguments);  
  21.         }  
  22.         return this;  
  23.     };  
  24.   
  25.     function createMediaPlayer(options){  
  26.        //mediaPlayer initialisation code  
  27.     }  
  28.   
  29.     //Exposed functions  
  30.     function play(url) {  
  31.       //code to play media  
  32.     }  
  33.   
  34.     function stop() {  
  35.       //code to stop media  
  36.     }  
  37. })(jQuery);  
When the plugin function is called the first argument is examined to see what type it is. If this is NOT a string it is a call to initialise a mediaPlayer and so the createMediaPlayer() method is executed by using the apply() command. It uses apply as it 1) ensures that the context is correct (this is passed as the context); and 2) allows us to pass the argument(s) without caring about how many there are.
If the first argument is a string, then this must be the name of the function that is required to be executed. A new arguments array is prepared by stripping off the first argument (see this blog post about splice), as the called method won’t need this. Then the named function is executed by calling apply() and passing the current context and our new list of arguments. You might notice that when this named function is executed it has to retrieved from the commands object by using the bracket notation. This is required for the apply() function to be able to execute functions dynamically from string values.
This is how we would initialise this plugin and call the play() function:
  1. $('.myDiv')  
  2.    .mediaPlayer(options);  
  3.   
  4. $('.myDiv')  
  5.    .mediaPlayer('play', url1);  
As the context is always returned from the plugin function we can still chain calls to our selected DIV:
  1. $('.myDiv')  
  2.    .mediaPlayer(options)  
  3.    .otherPlugin();  
  4.   
  5. //chain calls to our play method...  
  6. $('.myDiv')  
  7.    .mediaPlayer('play', url1)  
  8.    .mediaPlayer('play', url2);  

Conclusion

In this post I’ve looked at two mechanisms that can be used to expose functions on jQuery plugins. The first of these is the Class and Function Mechanism that exposes ‘public’ functions by returning an instance of a class from the plugin initialisation function. Although this does allow functions to be exposed, it does break jQuery chaining and so is not ideal.
The Execution Through apply() Mechanism still allows jQuery chaining as the current context is still returned from the plugin initialisation function. This mechanism allows access to functions by effectively overloading the initialisation function to allow the name of the function that is to be executed to be passed in. This function can then be executed by using the JavaScript apply() command.

Twitter Delicious Facebook Digg Stumbleupon Favorites More

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | Affiliate Network Reviews