Skip to content

Extending jQuery UI Widgets Revisited

This page is obsolete (it uses jQuery UI 1.5). Please see the updated page.

This is an updated version of a tutorial I wrote a bit back, improved thanks to conversations with Scott Gonzalez of the jQuery UI team. Thanks!

Avoiding Bloat in Widgets

A while back, Justin Palmer wrote an excellent article on "Avoiding Bloat in Widgets." The basic premise (no suprise to anyone whose ever dealt with object-oriented programming) is that your widgets should not do everything possible; they should do one thing well but be flexible enough to allow others to modify them.

He describes two ways of extending objects: subclassing and aspect-oriented programming (AOP). Subclassing creates new classes, while AOP modfies the methods of a single object. Both can be useful.

So let's make Palmer's Superbox widget (it just moves randomly about the screen with mouse clicks):


$.yi = {}; // create our namespace
var Superbox = {
	init: function(){
		var self = this;
		this.element.click(function(){
			self.move();
		});
	},
	move: function(){
		this.element.css (this.newPoint());
	},
	newPoint: function(){
		return {top: this.distance(), left: this.distance()};
	},	
	distance: function(){
		return Math.round (Math.random()*this.getData('distance'));
	}
};
$.widget ('yi.superbox', Superbox);
$.yi.superbox.defaults = { distance: 200 };

I've factored apart a lot of the code, so we have plenty of "hooks" to use to extend the method without copying code. Note that none of the code refers to "superbox" directly, so we can create subclasses that don't know the superclass's name.

Experiment 1 (Click Me)

Subclassing Widgets

Now let's make a new class, supererbox, that moves rather than jumps to its new location. I'll use Douglas Crockford's prototypal inheritance pattern to simplify subclassing (you could use a fancier one like Dean Edward's Base, or manipulate prototypes yourself).


function object(o){
	function F(){};
	F.prototype = o;
	return new F;
}

$.widget.subclass = function (name, superclass){
	$.widget(name); // Slightly inefficient to create a widget only to discard its prototype, but it's not too bad
	name = name.split('.');
	var widget = $[name[0]][name[1]]; // $.widget should return the object itself!
	
	widget.prototype = object(superclass.prototype);
	var args = Array.prototype.slice.call(arguments,1); // get all the add-in methods
	args[0] = widget.prototype;
	$.extend.apply(null, args); // and add them to the prototype
	widget.defaults = object(superclass.defaults);
	
	// Subtle point: we want to call superclass init and destroy if they exist
	// (otherwise the user of this function would have to keep track of all that)
	if (widget.prototype.hasOwnProperty('init')){;
	  var init = widget.prototype.init;
		widget.prototype.init = function(){
			superclass.prototype.init.apply(this);
			init.apply(this);
		}
	};
	if (widget.prototype.hasOwnProperty('destroy')){
		var destroy = widget.prototype.destroy;
		widget.prototype.destroy = function(){
			destroy.apply(this);
			superclass.prototype.destroy.apply(this);
		}
	}
	return widget; // address my complaint above
};

// allow for subclasses to call superclass methods
$.widget.prototype.callSuper = function(superclass, method){
	superclass = superclass.split('.'); // separate namespace and name
	return $[superclass[0]][superclass[1]].prototype[method].apply(this, Array.prototype.slice.call(arguments, 2)); // corrected from (arguments, 3)
};
// For the purposes of this demo, the above function was defined after we created superbox,
// so superbox does not include it. We'll include it manually
$.yi.superbox.prototype.callSuper = $.widget.prototype.callSuper;

And use it like this:


$.widget.subclass ('yi.supererbox', $.yi.superbox, {
	// overriding and new methods
	move: function(){
		this.element.animate(this.newPoint(), this.getData('speed'));
	},
	home: function(){
		this.element.animate({top:0, left:0}, this.getData('speed'));
	}
});
$.yi.supererbox.defaults.speed = 'normal';

The function signature is $.widget.subclass(name <String>, superclass <Object>, [newMethods <Object>]*), where you can use as many newMethod objects as you want. This lets you use mixin objects, like $.ui.mouse, that add a specific set of methods.

We now have a new widget called supererbox that is just like superbox but moves smoothly.

Experiment 2 (Click Me)

Calling Superclass Methods

If we want to use the superclass methods in our method, we use callSuper:


$.widget.subclass('yi.superboxwithtext', $.yi.supererbox, {
	move: function(){
		var count = this.getData('count') || 0;
		++count;
		this.setData('count', count);
		this.element.text('Move number '+count);
		this.callSuper('yi.supererbox', 'move'); // note that we could just as well use 'yi.superbox' for the original, "jumpy" move
	}
});
Experiment 3 (Click Me)

Aspect Oriented Programming

Aspect oriented programming addresses some of these problems, by keeping a reference to the superclass method. New methods don't so much override the old ones as supplement them, adding code before or after (or both) the original code, without hacking at the original class definition.

We'll create a mixin object for widgets that's stolen straight from Justin Palmer's article:


$.widget.aspect =  {
  yield: null,
  returnValues: { },
  before: function(method, f) {
    var original = this[method];
    this[method] = function() {
      f.apply(this, arguments);
      return original.apply(this, arguments);
    };
  },
  after: function(method, f) {
    var original = this[method];
    this[method] = function() {
      this.returnValues[method] = original.apply(this, arguments);
      return f.apply(this, arguments);
    }
  },
  around: function(method, f) {
    var original = this[method];
    this[method] = function() {
      this.yield = original;
      return f.apply(this, arguments);
    }
  }
};

And we use it just like we might use $.ui.mouse: $.widget('ns.foo', $.extend({}, {...methods for foo...}, $.widget.aspect) or, with our subclassing, $.widget.subclass('ns.foo-with-aspects', $.ns.foo, $.widget.aspect).

For example, let's say we have a cool plugin to make an element pulsate (I know, UI has a pulsate method that does this):


$.fn.pulse = function (opts){
	opts = $.extend ({}, $.fn.pulse.defaults, opts);
	for (i = 0; i < opts.times; ++i){
		this.animate({opacity: 0.1}, opts.speed).animate({opacity: 1}, opts.speed);
	}
	return this;
};
$.fn.pulse.defaults = {
	speed: 'fast',
	times: 2
};

And we'll create a version of supererbox that allows for AOP:


$.widget.subclass ('yi.supererbox2', $.yi.supererbox, $.widget.aspect);

And we'll create a supererbox object, then make it pulse before moving:


$('#experiment4').supererbox2().supererbox2('before','move', function() {
	this.element.pulse();
});
Experiment 4 (Click Me)

Or even make it pulse before and after moving:


$('#experiment5').supererbox2().supererbox2('around','move', function() {
	this.element.pulse();
	this.yield();
	this.element.pulse();
});
Experiment 5 (Click Me)

Note that once we added the aspect methods to create supererbox2, we didn't create any new classes to get this new behavior; we added the behavior to each object after the object was created.

{ 2 } Comments

  1. peschler | August 22, 2008 at 3:37 pm | Permalink

    Hi,

    great post. I’m currently using your code to create a wizard which derives from the ui.tabs widget and everything works like a charm! Thanks for saving me a lot of time.

    While using the callSuper() is spotted at bug – at least it did not work to call a superclass function with arguments until I corrected the following code:
    [code]
    var CallSuper = {
    callSuper: function(superclass, method){
    // return superclass.prototype[method].apply(this, Array.prototype.slice.call(arguments, 3));
    return superclass.prototype[method].apply(this, Array.prototype.slice.call(arguments, 2));
    }
    };
    [/code]

    That is: the slice should start at 2, otherwise the first argument will be sliced away.

  2. Danny | August 22, 2008 at 4:18 pm | Permalink

    @peschler:
    You’re absolutely correct (I’ve updated it). slice starts at 0!
    I will admit, though, that in my own code I removed callSuper in favor of using apply directly:
    $.yi.superbox.prototype.move.apply(this, arguments)
    I think it’s just as clear as this.callSuper(‘yi.superbox’, ‘move’)
    and communicates better what is going on. It’s more verbose, and I may change my mind.

    I wrote a recent post
    on my most recent code.

    Thanks for the feedback!

Post a Comment

Your email is never published nor shared. Required fields are marked *

Notify me of followup comments via e-mail. You can also subscribe without commenting.