• 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:

var pluginInstance = $('#foo').data('myplugin');
pluginInstance.publicMethod();

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:

(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);

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:

$.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'));
	});
};

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

$('#foo').myplugin().publicMethod();

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:

($('#foo').myplugin()[0]).publicMethod();
($('#foo').myplugin()[1]).publicMethod();

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:

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]();
				});
			};
		}
	}
}

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

$('#foo').myplugin().publicMethod();

it is functionally equivalent to:

$('#foo').myplugin().each(function(instance) { 
	instance.publicMethod();
});

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:

result.$ = this;

Which lets us do:

$('#foo').myplugin().$.addClass('bar');

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!

(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);

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

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

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!