Philippe Masset

Engineer at Buffer.

Architecture of a multi-instantiable jQuery plugin

December 2011

Multi-instantiable jQuery plugin

Edit (October 2014): I've learned a lot about JS and jQuery since first publishing this tutorial. It's still a valid basis, but lots of things can be improved. More importantly, a plugin's architecture can vary greatly depending on what you want it to be able to do. I'd advise you to have a look at the plugins I most recently (re)wrote, or take inspiration from the multitude of other jQuery plugin patterns.

Every jQuery developer, once familiarized with the language, wants to start creating plugins. That's an efficient and easy way to avoid duplicating code. Most of all, the result is awesomely thrilling to use: a simple $('p').goBananas() could transform all your paragraphs into flying bananas!

jQuery plugins 101

Let's start with the basics. If you already know them, you may jump over this part.

Anonymous function wrapper

First, when working with jQuery, a good practice is to wrap your code into an anonymous function:

(function($){
    // JavaScript code
})(jQuery)

This basically passes jQuery as a parameter to the anonymous function that contains your plugin code, overriding any global variable in this particular function's scope, thus ensuring that $ only refers to the value you gave your parameter: jQuery. That allows you to make sure there is no conflict with other JavaScript frameworks that might be using $ too.

jQuery.fn namespace

jQuery.fn is where your plugin is supposed to be stored.

We're going to create a small, useless plugin named color, that will allow us to interact with the background color of elements.

$.fn.color = function(){
    // plugin code
};

Here, we've created our plugin. It isn't doing anything yet, but it's there.

Parameters

We will want to pass parameters to our plugin, to use custom background colors for example. But we don't want to have to pass parameters each time we call our plugin, so we're going to use default values for these parameters.

Let's create an object inside $.fn.color that will store those default values:

$.fn.color.defaultOptions = {
    firstColor: '#0f0',
    secondColor: '#f00'
};

Our color plugin will, when called, change the background color of targeted elements to firstColor. And it will change it to secondColor through a function that we'll see later.

That's great, but now we'd want to be able to change these default values.

Here's how: add a parameter to $.fn.color, and use jQuery.extend() to override default values by those you specified. From the doc, jQuery.extend() allows us to "merge the contents of two or more objects together into the first object".

$.fn.color = function(options){
    options = $.extend({}, $.fn.color.defaultOptions, options);
    // plugin code
};

That way, the content of $.fn.color.defaultOptions will be merged with options, and will be stored in an empty object, which will then be returned (and stored back into options).

Alright, we're all set! We have our color plugin, and we can pass it parameters. Let's see what's next.

Work on targeted elements

Like I said earlier, the goal of the color plugin will be to change the background color of targeted elements.

A more programmatic way to say this would be "to give methods to each targeted elements for changing their background color". Notice the each.

Iterate over elements

$.fn.color = function(options){
    options = $.extend({}, $.fn.color.defaultOptions, options);
    // plugin code
    $(this).each(function(){
        // work on elements
    });
    return $(this);
};

The $(this) keyword here refers to a jQuery object that contains targeted DOM elements. $(this).each() allows us to iterate over these DOM elements.

Notice the return $(this); at the end of the loop. It's here to maintain chainability. Without it, you would not be able to do this for instance:

$("p").color().fadeOut();

Take care of each element

Inside the callback function used with $(this).each(), each element is referred to by the keyword $(this).

Let's think more precisely about what we're going to do with this plugin:

  • Step 1: When the plugin will be called on an element, it will set its background color to options.firstColor.
  • Step 2: After that, we'll bind a method to that element to alert its two colors on click (options.firstColor and options.secondColor).
  • Step 3: Finally, we'll add a method to that element that will replace its backgroud color with options.secondColor. We'll have to make this method triggerable from anywhere.

So yeah, we'll have to make this plugin multi-instantiable. By that, I mean that each targeted element will have its own collection of variables and methods, so that they can be accessed and called separately.

That's where it becomes interesting!

Three ways to work on targeted elements

When the plugin is called (step 1)

Context: We're inside the plugin's execution flow. $(this) refers to one targeted element, and we can easily access parameters from the options object.

We set the element's background color to options.firstColor.

// step 1
$(this).css('background-color', options.firstColor); 

When an element is clicked (step 2)

Context: We're outside the plugin's execution flow. A click event is triggered on an element. $(this) refers to the clicked element. We can't access the options object, since it's a parameter passed to the plugin, thus only accessible from its execution flow.

We have to alert the element's two colors (options.firstColor and options.secondColor). But how to do that, as they were stored in the options object and we don't have access to it?

We'll use the magnificent .data() from jQuery, that allows us to attach data to a DOM element.

When the plugin is called, we will create an object that will store every variable we need, and attach it to our element. Then, we'll just bind a function to the click event on that element. That function will get the data it needs right from the clicked element, and alert it.

// step 2
// store variables into the color object
var color = {
    element: $(this),
    firstColor: options.firstColor,
    secondColor: options.secondColor,
};
// attach the color object to the element
$(this).data("color", color);
// bind a function to the click event on the element
$(this).unbind(".color").bind("click.color", function(e){
    // get the data it needs right from the data attached to the element
    var color = $(this).data("color");
    // alert the data
    alert('First color: '+ color.firstColor +' - Second color: '+ color.secondColor);
});

You'll notice the .bind('click.color'): we're registering for the click event, using the color namespace. That way, we can safely use .unbind('.color') to unbind everything related to our plugin from this element, without taking the risk to unbind something that is not our business.

Call a private method from the outside (step 3)

Context: We're outside the plugin's execution flow. We can be anywhere. $(this) can refer to anything. We still can't access the options object.

You got that, we're not in a winning situation. Let's call step 2 to the rescue.

We already created an object that stores stuff, and attached it to our element. And we use it in step 2 to retrieve the value of some variables.

Let's do the same here! In JavaScript, functions are also considered objects. So let's store one inside the object we attach to our element!

That way, we can get and call this method right from our element. And, this method being called from inside the object that stores all our variables, we just have to use the this keyword to access all of them!

// step 2
var color = {
    element: $(this),
    firstColor: options.firstColor,
    secondColor: options.secondColor,
    // step 3
    changeColor: function(){
       this.element.css('background-color', this.secondColor); 
    }
};
$(this).data("color", color);

Now, calling this method is as simple as $('p').data('color').changeColor();.

Final plugin code

That's how our color plugin looks like after putting together all the pieces.

You can find a working demo here, that will make it easier to understand: http://jsfiddle.net/pioul/x9hWk/3/

(function($){
    $.fn.color = function(options){
        options = $.extend({}, $.fn.color.defaultOptions, options);
        $(this).each(function(){
            // step 1
            $(this).css('background-color', options.firstColor);
            // step 2
            var color = {
                element: $(this),
                firstColor: options.firstColor,
                secondColor: options.secondColor,
                // step 3
                changeColor: function(){
                   this.element.css('background-color', this.secondColor); 
                }
            };
            $(this).data("color", color);
            $(this).unbind(".color").bind("click.color", function(e){
                var color = $(this).data("color");
                alert('First color: '+ color.firstColor +' - Second color: '+ color.secondColor);
            });
        });
        return $(this);
    };
    $.fn.color.defaultOptions = {
        firstColor: '#0f0',
        secondColor: '#f00'
    };
})(jQuery)