Extending jQuery UI Widgets

View My GitHub Profile

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


var Superbox = {
	_init: function(){
		var self = this;
		this.element.on('click.superbox', 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.options.distance);
	},
	_destroy: function(){
		this.element.off('click.superbox');
	},
	options: {
		distance: 200
	}
};
$.widget ('bililite.superbox', Superbox);

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

The widget factory ($.widget) allows you to use one widget as the base for another.Under the hood, it uses an instance of the base class as the prototype for the new widget; see the source for $.widget for how it works, and my Deeper Look page for some details on the thinking that was eventually incorporated into $.widget.



$.widget('bililite.supererbox', $.bililite.superbox, {
	// overriding and new methods
	move: function(){
		this.element.animate(this._newPoint(), this.options.speed);
	},
	home: function(){
		this.element.animate({top:0, left:0}, this.options.speed);
	},
	options: {
		speed: 'normal'
	}
});

The function signature is $.widget(name [String], base-constructor [Object, should be $.namespace.widgetname], new-methods [Object] ). To use mixin objects, like $.ui.mouse, use $.extend({my-methods}, $.ui.mouse) for the new 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 this._super:


$.widget('bililite.superboxwithtext', $.bililite.supererbox, {
	move: function(){
		this.options.count = this.options.count || 0;
		++this.options.count;
		this.element.text('Move number '+this.options.count);
		this._super(); 
	}
});
Experiment 3 (Click Me)

this._super is called like javascript call: this._super(arg1, arg2, arg3). It's often easier to use the original arguments to the method, and for this there is a convenience method this._superApply which takes an Array or Array-like object, similar to javascript apply: this._superApply(arguments).

Extending Constructors and Destructors

You often want to extend the _create,_init and _destroy methods in a subclass. Generally, a subclass should be just like the superclass but with some modifications, so the superclass methods should always be called as well. Generally in _create and _init methods, you call this._super right at the beginning (effectively creating an instance of the superclass), then modify it. In the _destroy method, call this._super at the end, after undoing the subclass-specific stuff. Thus:


$.widget('bililite.superboxwithmouseover1', $.bililite.supererbox, {
	_init: function(){
		this._super();
		this.element.on('mouseenter.superbox', function(){
			$(this).css('background-color', 'blue');
		}).on('mouseleave.superbox', function(){
			$(this).css('background-color', 'green');
		});		
	},
	_destroy: function(){
		this.element.off('mouseenter.superbox mouseleave.superbox');
		this._super();
	}
});
Experiment 4 (Click Me)

The way C++ does it is to call the superclass constructor and destructor automatically, and I would like to do that:


(function(){
	var oldwidget = $.widget;
	$.widget = function(name, base, prototype){
		if ( !prototype ) {
			prototype = base;
			base = $.Widget;
		}
		var proto = $.extend({}, prototype); // copy it so it can be reused
		for (key in proto) if (proto.hasOwnProperty(key)) switch (key){
			case '_create':
				var create = proto._create;
				proto._create = function(){
					this.super();
					create.apply(this);
				};
			break;
			case '_init':
				var init = proto._init;
				proto._init = function(){
					this._super();
					init.apply(this);
				};
			break;
			case '_destroy':
				var destroy = proto._destroy;
				proto.destroy = function(){
					destroy.apply(this);
					this._super();
				};
			break;
		}
		return oldwidget.call(this, name, base, proto); // set up the widget as usual
	};
	$.widget.extend = oldwidget.extend;
	$.widget.bridge = oldwidget.bridge;
})();

And now we can override without calling _super:


$.widget('bililite.superboxwithmouseover2', $.bililite.supererbox, {
	_init: function(){
		this.element.on('mouseenter.superbox', function(){
			$(this).css('background-color', 'blue');
		}).on('mouseleave.superbox', function(){
			$(this).css('background-color', 'green');
		});		
	},
	_destroy: function(){
		this.element.off('mouseenter.superbox mouseleave.superbox');
	}
});

I use it. Your mileage may vary. The code is used in my flexcal project.

There is one other method that should always call _super: _setOption. The _setOption method in the widget should handle specific options for that widget, and the _super code handles any other options (at the very least, the base class $.Widget sets the values in the options object). But there's no way to know whether to call _setOption before or after the new code; you might want to constrain a value before it is set, or do a different action (and not call _super at all). So I don't automatically call it. Also, some of the official widgets use _super in _setOption, and they call it explicitly, so calling it again might cause problems.

Experiment 5 (Click Me)

Aspect Oriented Programming

Aspect oriented programming allows the user of an object to modify its behavior after it has been instantiated. 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 add methods for widgets that are stolen straight from Justin Palmer's article:


$.extend($.Widget.prototype, {
	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() {
			var tmp = this.yield;
			this.yield = original;
			var ret = f.apply(this, arguments);
			this.yield = tmp;
			return ret;
		}
	}
});

And now we can use these methods in our code.

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 supererbox object, then make it pulse before moving:


$('#experiment6').supererbox().supererbox('before','move', function() {
	this.element.pulse();
});
Experiment 6 (Click Me)

Or even make it pulse before and after moving:


$('#experiment7').supererbox().supererbox('around','move', function() {
	this.element.pulse();
	this.yield();
	this.element.pulse();
});
Experiment 7 (Click Me)

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