Understanding jQuery UI Widgets: A Tutorial

View My GitHub Profile

This was written largely to help me make sense of using UI to create my own widgets, but I hope it may help others. "Widget" to me means a user-interface element, like a button or something more complicated like a popup date picker, but in jQuery UI terms it means a class with some saved state, members of which are associated with HTML elements; things like Draggable and Sortable. In fact, not everything that I would have called a widget uses $.widget; the UI datepicker does not (though that's in the TODO list).

There is an official jQuery UI tutorial on widgets but since I wrote this tutorial back in 2009 it's been the most popular article on my blog, so I'm preserving it. You can see the original version.

Modifying Elements: Plugins

That being as it may, let's use $.widget.

Let's take a paragraph of class target:


	<p class="target">This is a paragraph</p>
	

And lets make it green. We know how; $('.target').css({background: 'green'}).

Now, make it more general-purpose: a plugin:

$.fn.green = function() {return this.css({background: 'green'})};

But this allows us to perform some behavior on the selected elements; it does not leave us with any way to keep our plugin associated with that element, so we can do something with it later, like $('.target').off() to remove the green background, but only if we used green to put it there in the beginning. We also have no way of associating state with the element, to do $('.target').darker(), which would require knowing how green the element is now.

Keeping State in Plugins

We could create an object and associate it with an element using javascript expandos: element.myobject = new Myobject({'target': element}). Sample code would be:


$.fn.green2 = function() {
	return this.each(function(){
			if (!this.green) this.green = new Green($(this)); // associate our state-keeping object with the element
			this.green.setLevel(15);
	});
};
$.fn.off = function() {
	return this.each(function(){
		if (this.green) this.green.setLevel(16);
		delete this.green; // recover the memory
	});
};
$.fn.darker = function() {
	return this.each(function(){
		if (this.green) this.green.setLevel(this.green.getLevel()-1);
	});
};
$.fn.lighter = function() {
	return this.each(function(){
		if (this.green) this.green.setLevel(this.green.getLevel()+1);
	});
};

function Green(target){
	greenlevels = ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0','#fff'];
	this.target = target; // associate the element with the object
	this.level = 0;
	this.getLevel = function() { return this.level; }
	this.setLevel = function(x) {
		this.level = Math.floor(Math.min(greenlevels.length-1, Math.max(0,x)));
		this.target.css({background: greenlevels[this.level]});
	}
};

But this pollutes the $.fn namespace terribly, with off, darker and lighter. There are ways to create real namespaces within $.fn, but the usual design pattern is to use a string to specify which function to call. Thus, element.green2() to instantiate the plugin, element.green2('darker') or element.green2('lighter') to manipulate it:


$.fn.green2 = function(which){
	return this.each(function(){
		if (which === undefined){ // initial call
			if (!this.green) this.green = new Green($(this)); // associate our state-keeping object with the element
			this.green.setLevel(15);
		}else if (which == 'off'){
			if (this.green) this.green.setLevel(16);
			delete this.green
		}else if (which == 'darker'){
			if (this.green) this.green.setLevel(this.green.getLevel()-1);
		}else if (which == 'lighter'){
			if (this.green) this.green.setLevel(this.green.getLevel()+1);
		}
	});
};

function Green(target){
	greenlevels = ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0', '#fff'];
	this.target = target; // associate the element with the object
	this.level = 0;
	this.getLevel = function() { return this.level; }
	this.setLevel = function(x) {
		this.level = Math.floor(Math.min(greenlevels.length-1, Math.max(0,x)));
		this.target.css({background: greenlevels[this.level]});
	}
};

	<p class="target">This is a test paragraph</p>
	

The Problems with Associating an Object with a Plugin

But you get into trouble with circular references (note that "this.green = new Green($(this))" gives a DOM element a reference to a javascript object and "this.target = target" gives a javascript object a reference to a DOM element) and memory leaks: browsers (notably Internet Explorer) uses different garbage collectors for DOM elements and javascript objects. Circular references mean that each garbage collector thinks the other object is in use and won't delete them.

We also need to remember to reclaim the memory (with delete) if we no longer need the plugin.

jQuery solves the circular reference problem with the $.fn.data plugin: $(element).data('myobject', new Myobject({'target': element})). But now we've got a lot of "paperwork" to keep track of, and it hides the underlying program logic. As we know, design patterns reflect language weakness. If we are constantly re-implementing a pattern, we need to abstract it and make it automatic.

Solving the Problem: $.widget

That's where $.widget comes in. It creates a plugin and an associated javascript class and ties an instance of that class with each element so we can interact with the object and act on the element, without getting into trouble with memory leaks.

You still need to create the constructor of your class, but instead of a real constructor function, you need a prototype object with all the relevant methods. There are a few conventions: the function _create is called on construction, _init is called both on construction and for re-initializing the function and _destroy is called on removal. All of these are predefined as empty functions but you can override them (and most likely will need to override _init). element is the associated jQuery object (what we called target above).

"Re-initializing" (that calls _init) is done by passing by calling the widget plugin on an element that already has that widget, as $(el).widget()or $(el).widget({option: value}). Calling it with a string, as $(el).widget('method', arg1, arg2) assumes an already-initialized widget (it will throw an error if not), and calls method(arg1, arg2).

Widget methods that start with "_" are pseudo-private; they cannot be called with the $(element).plugin('string') notation


var Green3  = {
	_init: function() { this.setLevel(15); },
	greenlevels: ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0', '#fff'],
	level: 0,
	getLevel: function() { return this.level; },
	setLevel: function(x) {
		this.level = Math.floor(Math.min(this.greenlevels.length-1, Math.max(0,x)));
		this.element.css({background: this.greenlevels[this.level]});
	},
	darker: function() { this.setLevel(this.getLevel()-1); },
	lighter: function() { this.setLevel(this.getLevel()+1); },
	_destroy: function() {
		this.element.css({background: 'none'});
	}
};

Notice it's all program logic, no DOM or memory-related bookkeeping. Now we need to create a name, which must be preceded by a namespace, like "ns.green" . Unfortunately the namespacing is fake; the plugin is just called $().green(). The constructor function is $.ns.green, but you almost never use that. The official recommendations read:

When building your own plugins, you should create your own namespace. This makes it clear where the plugin came from and if it is part of a larger collection.

So I will use the bililite namespace. Defining the widget couldn't be easier:


$.widget("bililite.green3", Green3); // create the widget

Manipulating Widgets

What about our manipulating functions? All the functions defined in the prototype that don't start with an underscore are exposed automatically: $('.target').green3() creates the widgets; $('.target').green3('darker') manipulates them.

There are two kinds of plugin functions: getters and setters. Setters manipulate elements and can be chained in jQuery code; they just return the jQuery object they started with. For example, $('.demo').css({color: 'red'}).text('stuff'). Getters return information and break the chain, like text = $('.demo').text(). widget distinguishes between the two by the return value: if your function returns any value other than undefined, it assumes that it is a getter and returns that value (for the first element in the jQuery object, just like all jQuery getter functions). If it does not return a value, then it is assumed to be a setter and is called on each element in the jQuery object, and the jQuery object itself is returned for chaining.


	<p class="target">This is a test paragraph.</p>
	

Pass arguments to the manipulating functions after the name: $('.target').green3('setLevel', 5).

Data for Each Widget

The astute reader will have noticed that level is a class variable; the same variable is used for every green3 object. This is clearly not what we want; each instance should have its own copy. $.widget defines an object this.options for per-widget data. Thus:


var Green4  = { 
	getLevel: function () { return this.options.level; },
	setLevel: function (x) {
		var greenlevels = this.options.greenlevels;
		var level = Math.floor(Math.min(greenlevels.length-1, Math.max(0,x)));
		this.options.level = level;
		this.element.css({background: greenlevels[level]});
	},
	_init: function() { this.setLevel(this.getLevel()); }, // grab the default value and use it
	darker: function() { this.setLevel(this.getLevel()-1); },
	lighter: function() { this.setLevel(this.getLevel()+1); },
	_destroy: function() {
		this.element.css({background: 'none'});
	},
	options: { // initial values are stored in the widget's prototype
		level: 15,
		greenlevels: ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0', '#fff']
	} 
};
$.widget("bililite.green4", Green4);

And on creating an instance of a widget, pass an options object (the way most plugins do) and override the defaults: $('.target').green4({level: 8}).

Note that I also put the list of colors into the defaults object, so it too can be overridden. This widget probably shouldn't be called "green" anymore!


	<p class="target">This is a test paragraph.</p>
	

	<p class="target">
	This is a test paragraph called with .green4({
		level:3,
		greenlevels: ['#000','#00f','#088', '#0f0', '#880', '#f00', '#808', '#fff']
	}).
	</p>
	

You can get any of the options with $().green4('option','level'), which in this case is the same as $().green4('getLevel'). Whether to have a dedicated getter is up to you.

You can also set options with $().green4('option','level', 10) however, that only sets the variable but does not do the action associated with it. You can override the default _setOption method to do some action with

_setOption: function (key, value){
	if (key === 'level') this.setLevel(value);
	if (key === 'greenlevels'){
		this.options.greenlevels = value;
		this.setLevel(this.getLevel()); // force the redraw
	}
	return this._super(key, value); // do the default action
}

Callbacks, or, Keeping the Lines of Communication Open

The programmer who is embedding our widget in his page may want to do other things when the widget changes state. There are two ways to alert the calling program that something has happened:

Tightly Coupled
The caller can provide a function to call at the critical point. jQuery jargon calls this a "callback;" it's used in animations and Ajax. We can create callback functions that the widget calls at critical points, and pass them to the widget-constructing plugin like any other option:

var Green5 = {
	setLevel: function(x){
		//...
		this.element.css({background: greenlevels[level]});
		var callback = this.options.change;
		if ($.isFunction(callback)) callback(level);
	},
	// ... rest of widget definition
};
$.widget("bililite.green5", Green5);

$('.target').green5({change: function(x) { alert ("The color changed to "+x); } });
Loosely Coupled
Also called the Observer Design Pattern, the widget sends a signal to the programming framework and the calling program informs the framework that it wants to know about the signal. Events like clicks and keystrokes work like this, and jQuery allows the widget to create custom events and for the calling program to bind an event handler to that custom event:

var Green5 = {
	setLevel: function(x){
		//...
		this.element.css({background: greenlevels[level]});
		this.element.trigger ('green5change', level);
	},
	// ... rest of widget definition
};
$.widget("bililite.green5", Green5);

$('.target').green5();
$('.target').bind("green5change", function(evt,x) { alert ("The color changed to "+x); });

$.widget allows both forms with the _trigger method. In a widget object, this._trigger(type, event, data) takes a type {String} with the name of the event you want (use some short verb, like 'change') and optionally a $.Event object (if you want to pass things like timestamps and mouse locations. Don't worry about event.type; _trigger changes it to the constructed event name), and any data to be passed to the handler. _trigger creates a custom event name of widgetName+type, like green6change (why it doesn't do type+'.'+widgetName the way jQuery expects is beyond me naming events this way has been the subject of some discussion), sets event.type = custom event name (creating a new $.Event if it was not provided) and calls this.element.trigger(event, data) and then looks for a callback with callback = this._getData(type) and calls it with callback.call(this.element[0], event, data).

Notice that this means the function signature is slightly different for the event handler and the callback if data is an array. element.trigger() uses apply to turn each item in the array into a separate argument. So this._trigger('change', 0, ['one', 'two']) requires an event handler of the form function(event, a, b) and a callback of the form function(event, data).

There's a TODO in the code to change the event name to include a colon, thus "green5:change" rather than "green5change" which would be prettier, but it's not there as of version 1.11.

In practice, it's not as complicated as it sounds. For example, using both methods:


var Green5  = {
	getLevel: function () { return this.options.level; },
	setLevel: function (x) {
		var greenlevels = this.options.greenlevels;
		var level = Math.floor(Math.min(greenlevels.length-1, Math.max(0,x)));
		this.options.level = level;
		this.element.css({background: greenlevels[level]});
		this._trigger('change', 0, level);
	},
	_init: function() { this.setLevel(this.getLevel()); }, // grab the default value and use it
	darker: function() { this.setLevel(this.getLevel()-1); },
	lighter: function() { this.setLevel(this.getLevel()+1); },
	off: function() {
		this.element.css({background: 'none'});
		this._trigger('done');
		this.destroy(); // use the predefined function
	},
	options: {
		level: 15,
		greenlevels: ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0', '#fff']
	}
};
$.widget("bililite.green5", Green5);

	<p class="target">This is a test paragraph with green level <span class="level">undefined</span>.</p> 
	

//  The on button above does the following:
$('.target').green5({
	change: function(event, level) { $('.level', this).text(level); } // callback to handle change event
});
$('.target').bind('green5done', function() { $('.level', this).text('undefined');alert('bye!') }); // event handler for done event

Involving the Mouse

Now, a lot of what we want to do with widgets involves mouse tracking, so ui.core.js provides a mixin object that includes lots of useful methods for the mouse. All we need to do is add the $.ui.mouse widget to our widget prototype:


var Green6 = {mouse-overriding function and widget-specific functions};
$.widget ('bililite.green6', $.ui.mouse, Green6);

And override $.ui.mouse's functions (_mouseStart, _mouseDrag, _mouseStop) to do something useful, and call this._mouseInit in your this._init and this._mouseDestroy in your this.destroy. The mouse defaults are automagically including in your options object; see the mouse code for details.

Let's add some mouse control to our greenerizer:


Green6 = $.extend({}, $.bililite.green5.prototype, { // leave the old Green5 alone; create a new object
	_init: function(){
		$.bililite.green5.prototype._init.call(this); // call the original function
		this._mouseInit(); // start up the mouse handling
	},
	destroy: function(){
		this._mouseDestroy();
		$.bililite.green5.prototype.destroy.call(this); // call the original function
	},
	// need to override the mouse functions
	_mouseStart: function(e){
		// keep track of where the mouse started
		this.xStart = e.pageX; // not in the options object; this is not something that can be initialized by the user
		this.levelStart = this.options.level;
	},
	_mouseDrag: function(e){
		this.setLevel (this.levelStart +(e.pageX-this.xStart)/this.options.distance);
	},
	options: {
		level: 15,
		greenlevels: ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0', '#fff'],
		distance: 10
	}
});
$.widget("bililite.green6", $.ui.mouse, Green6);

	<p class="target">This is a test paragraph with green level <span class="level">undefined</span>.</p>
	

The ever-alert reader will note what we've just done: subclassed green5 to make green6, including calls to "super" methods. This can to be abstracted out but that's a topic for another day.