• 22 . 08 . 10
  • Object oriented jQuery plugins have typically been hard to create in an elegant way. Here, we demonstrate how to design a clean jQuery plugin which allows for full encapsulation of data, and allows access to public methods without using the data object as a obvious proxy.

  • Tags

    , , , ,

  • StumbleUpon

Object Oriented jQuery Plugins

Update 2: This code is now on GitHub and has had some substantial improvements made. You should look at the latest code here and submit a pull request if you make any improvements!


Update: As pointed out in the comments, the first version of this code didn’t allow for methods with parameters. Although the code in the main post below is unchanged, the code linked at the bottom of the page and here has been updated to reflect some improvements, that now allow this and some other niceties. The main body of the post should still be worth reading for the derivation though!


I’ve recently begun the process of porting some javascript library code from Prototype to jQuery, and on the whole it hasn’t been too problematic. I really like the element-centric nature of jQuery, whereas Prototype is more like a excellent set of useful static methods. There were only a couple of things I found myself really missing, and they were enumerables, and elegant plugin encapsulation. The first problem was solved with Xavier Shay’s nice enumerables plugin. The second one was more of a challenge.

The lack of encapsulation for jQuery plugins seems to be a common frustration, and there are lots of queries on the web along the lines of “how do I add public methods to a jQuery plugin”. The best solution I found came from Hector Virgen, which allows you to do something like this:

[javascript]
var pluginInstance = $(‘#foo’).data(‘myplugin’);
pluginInstance.publicMethod();
[/javascript]

That’s pretty neat, but I didn’t really like the idea of having to go through the data object each time. A couple of other people in the comments felt the same way, but there didn’t seem to be any solutions forthcoming, so I had a crack. What follows is largely based on Hector’s code, so you should go and read that first before you go through this.

We start off with a basic plugin shell, slightly modified from Hector’s, per my taste:

[javascript]
(function($) {
var Celsus = Celsus || {};
Celsus.MyPlugin = function(element, options) {

// Private members
var elem = $(element);

var settings = $.extend({}, options || {});

// Private methods
function _privateMethod() {
console.log("This is a private method!");
}

return {
publicMethod: function() {
console.log(elem);
_privateMethod();
return true;
}
};
};

$.fn.myplugin = function(options) {
return this.each(function() {
var element = $(this);
if (element.data(‘myplugin’)) {
return;
}
var myplugin = new Celsus.MyPlugin(this, options);

// Store the new plugin definition in a data object.
element.data(‘myplugin’, myplugin);
});
};
})(jQuery);
[/javascript]

This is a pretty good start. However, as Hector points out, the main issue is that the plugin returns a jQuery object to enable chaining. In many complex instances, chaining isn’t necessarily something you’re going to want to do, so we make a small sacrifice and forego that convenience. Instead, we are going to return a set of plugin instance objects:

[javascript]
$.fn.myplugin = function(options) {
var result = [];
this.each(function() {
var element = $(this);

if (!element.data(‘myplugin’)) {
// Initialise
var myplugin = new Celsus.MyPlugin(this, options);

// Store the new functions in a validation data object.
element.data(‘myplugin’, myplugin);
}
result.push(element.data(‘myplugin’));
});
};
[/javascript]

At this point, it’s not looking too great. We’ve broken chaining because we no longer return a jQuery object, but if you try and call

[javascript]
$(‘#foo’).myplugin().publicMethod();
[/javascript]

it still doesn’t work. This is because, although each plugin instance has the publicMethod() function, they are contained inside a bare array. At this point we could actually do:

[javascript]
($(‘#foo’).myplugin()[0]).publicMethod();
($(‘#foo’).myplugin()[1]).publicMethod();
[/javascript]

Or something similar with each(), but this is very messy. We need a bit of syntactic sugar. To achieve this, the next step is to take that array, turn it into something we can work with, and add a facade, so that every public function we’ve just mixed in is presented as an option on the plugin instance set. We do that by adding the following to the plugin definition:

[javascript]
result = $(result);
var template = result[0];
if (template) {
for ( var i in template) {
if (typeof (template[i]) == ‘function’) {
result[i] = function() {
this.each(function() {
this[i]();
});
};
}
}
}
[/javascript]

Firstly, we convert the array to a jQuery object. Then, we look at the first instance in the set and use that as a template. It should contain all the public functions we’ve defined in our definition class, and all the instances are of the same type, so we can safely use the first entry’s template for all of them. We then enumerate through all the public functions and create a proxy or facade function on the set, which simply calls the closure of that function for each element in the set. This ensures that when you call

[javascript]
$(‘#foo’).myplugin().publicMethod();
[/javascript]

it is functionally equivalent to:

[javascript]
$(‘#foo’).myplugin().each(function(instance) {
instance.publicMethod();
});
[/javascript]

So that’s pretty cool. We’ve avoided namespace pollution, we can mix in any number of public methods, and private members and methods behave as you’d expect them to. It isn’t possible to access public variables in this manner of course, but that is easily remedied by using public getters and setters. We can even add in a reference back to the jQuery object, so we can get chaining back in in some form:

[javascript]
result.$ = this;
[/javascript]

Which lets us do:

[javascript]
$(‘#foo’).myplugin().$.addClass(‘bar’);
[/javascript]

This is useful when you’re doing plugin initialisation, but thereafter it’s a bit redundant as the actual plugin call simply returns an object which you then ignore. The only final point is that there’s quite a lot of boilerplate going on here just to get set up. In actual fact, the actions are pretty generic, so we can extract all that code and put it in its own plugin. A plugin to generate a plugin!

[javascript]
(function($) {
$.fn.encapsulatedPlugin = function(plugin, definition, objects, options) {
var result = [];
objects.each(function() {
var element = $(this);

if (!element.data(plugin)) {
// Initialise
var instance = new definition(this, options);

// Store the new functions in a validation data object.
element.data(plugin, instance);
}
result.push(element.data(plugin));
});

// We now have a set of plugin instances.
result = $(result);

// Take the public functions from the definition and make them available across the set.
var template = result[0];
if (template) {
for ( var i in template) {
if (typeof (template[i]) == ‘function’) {
result[i] = function() {
this.each(function() {
this[i]();
});
};
}
}
}

// Finally mix-in a convenient reference back to the objects, to allow for chaining.
result.$ = objects;

return result;
};

})(jQuery);
[/javascript]

With this little plugin, our plugin initialisation code is a lot lighter:

[javascript]
$.fn.myplugin = function(options) {
return $.fn.encapsulatedPlugin(‘myplugin’, Celsus.MyPlugin, this, options);
};
[/javascript]

Not too bad! The actual code that specialises a plugin is tucked away in a neatly encapsulated object, we have access to all the public methods defined on it and multiple instances can happily live side by side and be invoked separately without trampling on each other. You can grab a copy of the plugin generating plugin from here. This is still new cod and there might be the odd glitch, so if you spot any, or have other ideas, be sure to leave a comment!

24 responses to “Object Oriented jQuery Plugins”

  1. majelbstoat says:

    Stoat – Where?: Object Oriented jQuery Plugins http://bit.ly/cmeiqa

    This comment was originally posted on Twitter

    • Cai Zhenrong says:

      Hi,

      I realize that if objects is empty…
      i.e. nothing is return from $(selector) when you do a $(selector).myplugin();

      result = result[0]; // result[0] is undefined here. which means result.$ = objects; will throw an error..

      so i added a check right in the beginning to return nothing…

      (function ($) {
          $.fn.encapsulatedPlugin = function (plugin, Definition, objects, options) {
             
              // steal this from Jörn Zaefferer’s Validation plugin http://bassistance.de/jquery-plugins/jquery-plugin-validation/

              if (!objects.length) {
                  options && options.debug && window.console && console.warn(“nothing selected, can’t create, returning nothing”);
                  return;
              }

      Let me know your thoughts/opinions… =D

  2. Webmaster Sh says:

    Hi,

    great article. I allready used this approach. But what about method parameters? Don’t they get lost by encapsulating the plugin?

    • Jamie Talbot says:

      You’re right, – this first implementation doesn’t handle that. But we
      can rectify that by changing the proxy function to use apply() with
      arguments. I’ll be doing that in the next version, thanks!

  3. This is a great improvement and excellent use of facade.

  4. Jonathan Skolnick says:

    Thanks for the plugin! It’s providing just what I need. I have profited.

    You mention above that “It isn’t possible to access public variables in this manner of course, but that is easily remedied by using public getters and setters.”

    Any chance you could provide an example of how to use public getters and setters? I’ve given it a shot to no avail.

  5. Jamie Talbot says:

    Hi Jonathon,

    Glad it’s been useful! Simplest way to do what you want would be like this:

    DemoPlugin = function() {
    var _size;

    return {
    getSize: function() {
    return _size;
    },

    setSize: function(size) {
    _size = size;
    }
    }
    }

    $.fn.myplugin = function(options) {
    return $.fn.encapsulatedPlugin('demoplugin', DemoPlugin, this, options);
    };

    and then using:

    $('#foo').demoplugin().setSize(55);
    $('#foo').demoplugin().getSize(); // returns 55.

    (All untested, but should be good!)

    Thanks for stopping by!

    • Jonathan Skolnick says:

      Jamie,

      Thanks for the response.

      I implemented as you suggested but am still not having success when trying to return a value using getters/setters. Any value I attempt to return is undefined.

      If you’re able to test it out I’d be interested to hear your results…

      Appending my code below in case you’re curious. I’m currently able to store an array that’s created in the encapsulated plugin using jquery .data, and accessing that as needed, but it feels like a bit of hack.

      Any thoughts appreciated.

      — my code —

      //from the ‘parent’ object
      $(this).format_people().setArray(‘– testing here — ‘);
      var return_test = $(this).format_people().getArray();
      log(‘return_test = ‘ + return_test);//returns undefined

      //
      Module_Image_Formatter_People = function() {

      var _content_array;

      function People_class(arrayElementNumber, thumb, employee_id, firstname, lastname, role, direct_phone, five_digit_dialing, bio_excerpt, email){
      this.arrayElementNumber=arrayElementNumber;
      this.thumb=thumb;
      this.employee_id=employee_id;
      this.firstname=firstname;
      this.lastname=lastname;
      this.role=role;
      this.direct_phone=direct_phone;
      this.five_digit_dialing=five_digit_dialing;
      this.bio_excerpt=bio_excerpt;
      this.email=email;
      }

      return {

      getArray: function() {
      return _content_array;
      },

      setArray: function(content_array) {
      _content_array = content_array;
      },

      parse_XML: function(xml) {

      var dynamic_content_objects_array = new Array();

      var i=0;

      $(xml).find(“entry”).each(function(){

      dynamic_content_objects_array[i] = new People_class(
      /* 0 */ i,
      /* 1 */ $(this).find(“link[rel*=’thumb’]”).attr(‘href’),
      /* 2 */ $(this).find(“[nodeName=employeesvc:employeeId]”).text(),
      /* 3 */ $(this).find(“[nodeName=employeesvc:firstName]”).text(),
      /* 4 */ $(this).find(“[nodeName=employeesvc:lastName]”).text(),
      /* 5 */ $(this).find(“[nodeName=employeesvc:jobTitle]”).text(),
      /* 6 */ $(this).find(“[nodeName=employeesvc:phoneDirect]”).text(),
      /* 7 */ $(this).find(“[nodeName=employeesvc:phone5Digit]”).text(),
      /* 8 */ $(this).find(“[nodeName=employeesvc:biography]”).text(),
      /* 9 */ $(this).find(“[nodeName=employeesvc:email]”).text()
      );

      i++;
      });

      log(“EXTERNALIZED Parsing = ” + dynamic_content_objects_array);

      //we store the following array using .data as we can not return the value for reasons unknown
      $(‘#module_people_01’).data(‘array_transfer’, dynamic_content_objects_array);

      },

      create_HTML_formatted_content: function(array_to_use, display_mode, i, number_of_grid_items) {

      //prepend the string that we will return with

        var content_in_HTML_format = “
        “;
        //if in multi-view mode
        if(display_mode==”multi”){
        log(“####### i = ” + i);
        var number_range_of_active_set = i + number_of_grid_items;
        log(“number_range_of_active_set = ” + number_range_of_active_set);

        for (var j=i; j array_to_use.length – 1){
        content_in_HTML_format += ‘

      • ‘;
        } else {
        content_in_HTML_format += ‘

      • ‘ + array_to_use[j].firstname + ” ” + array_to_use[j].lastname + ‘
        ‘ + array_to_use[j].direct_phone + ‘
        ‘ + array_to_use[j].five_digit_dialing + ‘

        ‘;
        }
        }
        //if in single view mode
        } else {
        content_in_HTML_format += ‘

      • ‘ + array_to_use[i].firstname + ” ” + array_to_use[i].lastname + ‘
        ‘ + array_to_use[i].role + ‘
        ‘ + array_to_use[i].direct_phone + ‘
        ‘ + array_to_use[i].five_digit_dialing + ‘
        ‘ + array_to_use[i].bio_excerpt + ‘

        ‘;
        }

        content_in_HTML_format += “

      “;

      $(‘#module_people_01’).data(‘content_transfer’, content_in_HTML_format);

      }

      }//end return

      }//end wrapper function

  6. Roger says:

    How does this compare to jQuery’s UI Widget Factory as an approach for creating stateful plugins?

    • Jamie Talbot says:

      Hey Roger, thanks for stopping by. I don’t know too much about the UI Widget Factory code, so can’t comment in depth, but it seems like a more full-featured solution than the technique I’ve outlined here. While my approach does allow for ‘state’ and allows for a nice encapsulated syntax like $element.plugin().action(), it doesn’t do anything to do with triggers and callbacks like the Widget factory code. It could be made to, of course, but it was only really intended as a mechanism for abstracting away some boilerplate code and allow for some rudimentary namespacing of methods.

  7. […] looking at Jamie Talbot’s technique for OOP in jQuery. I pretty much understand this for the most part, however I’m getting slightly lost when […]

  8. […] a recent post, I outlined a method to abstract away the complexity of creating an encapsulated jQuery plugin. […]

  9. sabel says:

    Thank you very much! This is awesome! Helped me a lot! 🙂

  10. luke says:

    is the proxy able to support inherited methods, for example if you used John Resig’s simple inheritance? http://ejohn.org/blog/simple-javascript-inheritance/

    • Jamie Talbot says:

      Hi Luke,

      It doesn’t currently – no _super() for example, but there’s no reason this couldn’t be extended to provide inheritance support. John’s own implementation is only 25 lines, and the mechanism he uses to create the _super() method is very similar to what I’ve done here, so it could be folded in pretty easily. Might be something I add in (or you could fork and push it yourself!).

      Cheers,

      Jamie.

      • luke says:

        Okay, that would be great. I’m still trying to wrap my head around inheritance in javascript but maybe I’ll give it a bash. Would be great to see an implementation from you since I doubt mine will be very elegant.

        Thanks!

  11. Kaine Varley says:

    Great work. More examples would be brilliant.

    Kaine

  12. Kaine Varley says:

    Hi Jamie,

    Thanks again for the great plug-in, works like a charm. On the subject of exposing getters and setters, I added the following code to my plug-in:

        //
        // EXPOSE PUBLIC MEMBERS RETURN
        //
        return {
          collapseAll       : function() { CollapseAll(); return true; },
          expandAll         : function() { ExpandAll(); },
          getSelectedItem   : function() { return _selectedItem; },
          getSelectedIndex  : function() { return _selectedItem.data(“index”); },
          getSelectedLevel  : function() { return $(_selectedItem).data(“level”); }
        }

    _selectedItem is set each time an item is clicked, and a callback method is executed. I have placed the following code in that callback function:

        function AlertItem() {
          alert(“level: ” + grid.getSelectedLevel + “nindex: ” + grid.getSelectedIndex);
        }

    Where “grid” is a global variable set when the plug-in is initialised. The attached screenshot is what appears in the alert dialog after an item has been selected and the callback method has been executed. Interestingly, though, is that both the “collapseAll” and “expandAll” methods work fine.

    Just the last little piece to this jigsaw, and I’d be grateful if you could provide it.

    Cheers,

    Kaine

  13. Benibex says:

    Hi Jamie! Awesome little plugin you’ve got here. How do you access the jQuery object that is invoking the plugin?

    In other words, if I call $(#foo).myplugin().publicMethod() and inside of public method I want to do something to the jQuery object selected by $(#foo), how would I do that?

    I was thinking it would be, e.g., this.$.width(). However, that doesn’t work…

    • Benibex says:

      OK, I think I figured it out.

      I was declaring my plugin definition like this:
      MyPluginDefinition = function() {
         /* stuff goes here */
      }

      So I was ignoring the “element” and “option” variables that you pass in.

      But if I modify it to actually use what you pass in, then I can pick up the element and use it with the normal jQuery calling convention. For example…

      MyPluginDefinition = function(element, options) {
          function _storeWidth() { var width = $(element).width(); }
      }

  14. Rich says:

    I was getting “undefined” for Setters & Getters. To resolve this I added a ‘return’ to line 54 of the source.
    BEFORE:set[i][f].apply(set[i][f], arguments);AFTER:return set[i][f].apply(set[i][f], arguments);The Github URL 404’d, so I’m using http://jamietalbot.com/projects/js/jquery/encapsulatedPlugin.js.txt.

    Great plugin btw.

Leave a Reply

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