Authoring jQuery Plugins With Object Oriented JavaScript

By Ryan Florence, published 2010-10-05

Part of the issue Thoughtful jQuery Plugin Development.

I don’t know if $.fn was ever intended to encapsulate all of your plugin logic or if it was merely an interface to add your logic to the jQuery API. Regardless of its original design, I consider its purpose the latter.

Instead of wrapping all the plugin logic inside the $.fn.disables = fn , keep all the logic outside in its own object and just use effin’ to add your object to the jQuery API. I get thousands of visits a month to my site from the keywords “object oriented javascript.” It makes me chuckle a little because JavaScript already is object oriented, it is designed to allow you build atomic chunks of code to use wherever you need them, stored in objects with prototypes.

If you’ve been reading the other articles in this issue, you’ll know I’m complaining about the vast majority of jQuery plugins being immutable, meaning, I can’t alter their behavior to meet my requirements without hacking the source. This is another iteration of my disables script. It disables form elements conditionally upon the attribute of another element, like a checkbox.

// first we set up our constructor function var Disabler = function(elements, subjects, options){ this.elements = elements; this.subjects = subjects; this.options = jQuery.extend({}, this.defaults, options); this.attach(); }; Disabler.prototype = { // now we define the prototype for Disabler defaults: { attr: 'checked', expected: true, onChange: function(){} }, test: function(element){ var isEqual = jQuery(element).attr(this.options.attr) == this.options.expected; this.subjects.attr('disabled', isEqual); this.options.onChange.apply(this, [isEqual, this.elements]); }, attach: function(){ var self = this; this.elements.each(function(index, element){ jQuery(element).bind({ 'change.Disabler': function(){ self.test(element); }, 'keyup.Disabler': function(){ self.test(element); } }); }); } }; // does nothing more than extend jQuery jQuery.fn.disables = function(subjects, options){ new Disabler(this, jQuery(subjects), options) return this; };

Extending is now possible, and is always awesome

To add a new method to all of my instances of Disabler I can do something like this:

Disabler.prototype.somethingElse = function(){ // do something new };

What I’m really after here is being able to extend a plugin with more sugar without having to hack the source. Let’s say I want to attach to the click event as well.

Disabler.prototype._attach = Disabler.prototype.attach; // store the old attach method Disabler.prototype.attach = function(){ // redefine it this._attach(); // do the old stuff first // do some new stuff var self = this; this.elements.each(function(index, element){ jQuery(element).bind({ 'click.Disabler': function(){ self.test(element); }, }); }); };

We can also go nuts and create a sort of child constructor of Disabler.

var SubDisabler = function(){ Disabler.apply(this, arguments) }; SubDisabler.prototype = $.extend({ log: function(){ console.log('I inherited stuff from Disabler') }, forrealz: function(){ console.log("I even copied that _attach override, if it was defined before me"); } }, new Disabler);

Doing these kinds of things help to keep all of the JavaScript logic outside of the application, in small, atomic, easily testable, chunks. If you fix a bug in Disabler, you fix it in every instance and in every “sub class.” Indeed, the same can be said of new features.

There are other tools for creating inheritance in JavaScript that be used with code that looks like this. check out John Resig’s simple inheritance or moo4q.

Instantiate objects with vanilla JavaScript

This also allows you to create an instance with normal old JavaScript instead of the jQuery API:

var disabler = new Disabler($('#some-checkbox'), $('.group')); disabler.test('#some-el');

Doing this really shines in larger code bases where the jQuery API starts to get in the way. I’ve argued before that using jQuery instead of vanilla JavaScript to manage the state of objects essentially turns jQuery into an awkward DSL. It handles the DOM great, but objects? I dunno. Admittedly, this is a matter of preference. Do what you like, I’m not going to get upset–after all, I did create moo4q and jQuery.addObject for this explicit purpose.

Hey! you’re in the global namespace!!!112123

I know, three or four of you are frighteningly ticked about the global namespace thing. Keep in mind that the jQuery.fn is a “continental” namespace that now carries with it the same problems that the “global” namespace does. There’s only one real way around the issue–put the developer in charge of the namespace, like this:

var Disabler = this.Disabler = function(elements, subjects, options){ // blah blah }; Disabler.prototype = { // blah blah }; jQuery.fn.disables = function(subjects, options){ new Disabler(this, jQuery(subjects), options); return this; };

Notice line 1: var Disabler = this.Disabler ... . By writing your plugin like this, you put the developer in control of where the object gets namespaced. Here’s how s/he or their package manager can put it on a namespace of their choosing:

var global = {}; // I'm'a use this instead of window (function($){ var Disabler = this.Disabler = function(elements, subjects, options){ // blah blah }; Disabler.prototype = { // blah blah }; jQuery.fn.disables = function(subjects, options){ new Disabler(this, jQuery(subjects), options); return this; }; }).apply(global, [jQuery]); // oh the magic of JavaScript!

Using the apply method on a function allows you to set the context inside the function, and then you pass in the arguments as an array. Magically, this becomes whatever you put for the first argument of apply . And now to extend it like in the last example:

global.Disabler.prototype._test = global.Disabler.prototype.test; global.Disabler.prototype.test = function(element){ this._test(element); console.log("I give a hoot, I didn't pollute!"); };

One last thing

I also like to provide a way for the developer to interact with my plugin through the jQuery API, check out jQuery.addObject - Managing a plugin’s state with the jQuery API to find out how. I know, I just said earlier I prefer to just use regular JavaScript for this, but it’s a matter of preference and I care about the folks using my code :)