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.
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.
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();
}
});
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)
.
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();
}
});
(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.
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();
});
Or even make it pulse before and after moving:
$('#experiment7').supererbox().supererbox('around','move', function() {
this.element.pulse();
this.yield();
this.element.pulse();
});
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.