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

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, 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.

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>

This is a test paragraph

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'];
	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(15, 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);
		}
	});
};

This is a test paragraph.

The Problems with Associating an Object With a Plugin

But you get into trouble with circular references (note 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 init is called on construction, the function destroy is called on removal. Both of these are predefined but you can override them (and most likely will need to override init). element is the associated jQuery object (what we called target above).


var Green3  = {
	greenlevels: ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0', '#fff'],
	level: 0,
	init: function() { this.setLevel(15); },
	getLevel: function() { return this.level; },
	setLevel: function(x) {
		this.level = Math.floor(Math.min(16, 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); }
	off: function() {
		this.element.css({background: 'none'});
		this.destroy(); // use the predefined function
	}
};

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 "yi.green" . Unfortunately the namespacing is fake; the plugin is just called $().green(). The constructor function is $.yi.green, but you never use that. But defining the widget couldn't be easier:

$.yi = $.yi || {}; // create the namespace
$.widget("yi.green3", Green3); // create the widget

(Yes, needing to create the namespace is a mistake that the authors of $.widget forgot.)

Manipulating Widgets

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

If your function is intended to be a "getter"; something that returns a value rather than manipulates the objects (like $('.target').html() returns the innerHTML) then you need to tell the widget that by assigning a list of names (space or comma-delimited) or array of names. Note that only the value for the first element in the jQuery object will be returned; exactly like .html() or .val().

$.yi.green3.getter = "getLevel otherGetter andAnother";
// or
$.yi.green3.getter = "getLevel, otherGetter, andAnother";
// or
$.yi.green3.getter = ["getLevel","otherGetter","andAnother"];

This is a test paragraph.

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 two more functions that let us store and retrieve data for each instance individually: setData and getData. Note that these are functions of the widget object, not the jQuery one. Thus:

function getLevel() { return this.getData('level'); }
function setLevel(x) {
	var level = Math.floor(Math.min(16, Math.max(0,x)));
	this.setData('level', level);
	this.element.css({background: greenlevels[level]});
}

And you can set initial values for the data with the defaults object: $.yi.green4.defaults = {level: 15} and override the defaults for any given object by passing an options object to the widget constructor: $('.target').green4({level: 8}).

(function($){
	// widget prototype. Everything here is public
	var Green5  = {
		getLevel: function () { return this.getData('level'); },
		setLevel: function (x) {
			var greenlevels = this.getData('greenlevels');
			var level = Math.floor(Math.min(greenlevels.length-1, Math.max(0,x)));
			this.setData('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); },
		off: function() {
			this.element.css({background: 'none'});
			this.destroy(); // use the predefined function
		}
	};
	$.yi = $.yi || {}; // create the namespace
	$.widget("yi.green5", Green5);
	$.yi.green5.defaults = {
		level: 15,
		greenlevels: ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0', '#fff']
	};
})(jQuery);

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!

This is a test paragraph.

Callbacks

Now, the user of our widget may want to do other things when our widget changes. We can create callback functions that the widget calls at critical points:

var Green6 = {
	setLevel = function(x){
		var greenlevels = this.getData('greenlevels');
		var level = Math.floor(Math.min(greenlevels.length-1, Math.max(0,x)));
		this.setData('level', level);
		this.element.css({background: greenlevels[level]});
		var callback = this.getData('change');
		if (isFunction(callback)) callback(x);
	},
	// ... rest of widget definition
};
$.widget("yi.green6", Green6);

$('.target').green6({change: function(x) { alert ("The color changed!"); } });

Or we can use a real observer pattern with custom events:

var Green6 = {
	setLevel = function(x){
		var greenlevels = this.getData('greenlevels');
		var level = Math.floor(Math.min(greenlevels.length-1, Math.max(0,x)));
		this.setData('level', level);
		this.element.css({background: greenlevels[level]});
		this.element.triggerHandler("green6changed", [x]);
		// this.element.trigger("green6changed", [x]) would work as well, but be a bit slower; it tries and fails to
		// trigger the native "green6changed" event, which doesn't exist, and does a element.each(...) rather than
		// element[0] (both are equivalent, since element only has one item, but the each wastes time figuring that out).
	},
	// ... rest of widget definition
};
$.widget("yi.green6", Green6);

$('.target').green6().bind("green6changed", function(e,level) { alert ("The color changed!"); });
// note the function convention for an event handler: first parameter is the event, the rest are passed from the trigger.
// for a custom event, when no real event is passed in, jQuery synthesizes a fake one.

$.widget encourages both forms. trigger and triggerHandler accept a third parameter, which should be the callback supplied by the user. jQuery will take care of the isFunction() test.

	this.element.triggerHandler("green6changed", [level], this.getData("change"));

And the user can use either the callback or the custom event method. However, there is a difference: custom event handlers get a synthetic event as the first parameter; callbacks only get the array explicitly given in the trigger call. If your callback needs event info (like mouse position), pass it directly: this.element.triggerHandler("green6changed", [eventObject, level], this.getData("change"));.

The convention that UI uses is to have the callback name be a verb and the event name be a short prefix followed by the verb.

This is a test paragraph with green level undefined.

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 object to our widget prototype:

Green7 = $.extend({}, Green6, $.ui.mouse);

And override $.ui.mouse's functions (mouseStart, mouseDrag, mouseStop) to do something useful, and call this.mouseInit in your this.init.

Let's add some mouse control to our greenerizer:

	Green7 = $.extend({}, $.yi.green6.prototype, $.ui.mouse); // leave the old Green6 alone; create a new object
	// need to override the mouse functions after they are added to the object
	Green7.mouseStart = function(e){
		this.setData('xStart', e.pageX);
		this.setData('levelStart', this.getData('level'));
	};
	Green7.mouseDrag = function(e){
		this.setLevel(this.getData('levelStart') +(e.pageX-this.getData('xStart'))/this.getData('distance'));
	}
	$.yi = $.yi || {}; // create the namespace
	$.widget("yi.green7", Green7);
	$.yi.green7.defaults = {
		level: 15,
		greenlevels: ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0', '#fff'],
		distance: 10
	};

This is a test paragraph with green level undefined.

The ever-alert reader will note what we've just done: subclassed green6 to make green7. This ought to be abstracted out into its own method, something like $.widget.subclass("yi.green7", "yi.green6", $.ui.mouse, {mouseStart: function(){}, mouseDrag: function(){}}) but that's a topic for another day.

10 Comments

  1. Cristiano Verondini says:

    Great tutorial. I did read ui.widget source, but with your help now everything fits in place. Thanks.

  2. Marcus Cavanaugh says:

    Well-written and useful. This should be required reading for people getting started with jQuery widgets.

  3. J?rn Zaefferer says:

    The datepicker will be refactored in the next release (1.7) to use the widget API, too. Namespaces are now automatically created, consider that fixed (since 1.6rc4).

    Otherwise, great tutorial. I’ve added a link to it from the Developer Guide wiki page: http://docs.jquery.com/UI/Developer_Guide#Widgets

  4. Karthik says:

    Great article! It does appear that in the latest release, they have changed from init -> _init
    though. You may want to update code snippets.

  5. Danny says:

    I know they’ve changed things for 1.6, but until it’s released, I’m going to stick with 1.5 (I’m using the Google code loader, and it’s still on 1.5) (This means I’m also stuck with jQuery 1.2.6 for now, unfortunately)

  6. Timothy Wall says:

    I notice that in the last example (Green7), mouse tracking is not removed in response to “off”. It probably should be, and would be a good example of extending the “destructor” .

  7. Danny says:

    @Timothy Wall:
    Good point. I’m updating this for jQuery UI 1.6 and I’ll make sure it all works. While the concepts are correct, this page is obsolete, since it assumes jQuery 1.2.6 and UI 1.5.3.

    Danny

  8. Rostyk says:

    xD

    the part “Data for Each Widget”

    the page fires js error for me as soon as I click “dark” button:

    TypeError: Object # has no method ‘getData’ [http://bililite.com/blog/2008/08/03/jquery-ui-widgets:374]

  9. Rostyk says:

    ok
    despite the fact there’re some minor issues in examples I’ve got lot of pleasure while reading this article

    thanks a lot to the author

  10. Danny says:

    @Rostyk:
    You’re reading a very old (2008!) blog post. The updated version is at http://bililite.com/blog/understanding-jquery-ui-widgets-a-tutorial/ .
    But I’m glad you enjoyed it.
    –Danny

Leave a Reply


Warning: Undefined variable $user_ID in /home/public/blog/wp-content/themes/evanescence/comments.php on line 75