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.
Cristiano Verondini says:
Great tutorial. I did read ui.widget source, but with your help now everything fits in place. Thanks.
August 5, 2008, 10:15 amMarcus Cavanaugh says:
Well-written and useful. This should be required reading for people getting started with jQuery widgets.
January 1, 2009, 11:13 pmJ?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
January 2, 2009, 7:59 amKarthik says:
Great article! It does appear that in the latest release, they have changed from init -> _init
January 20, 2009, 1:05 amthough. You may want to update code snippets.
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)
January 20, 2009, 3:13 amTimothy 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” .
January 26, 2009, 9:13 amDanny 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
January 26, 2009, 3:21 pmRostyk 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]
September 11, 2013, 4:15 amRostyk 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
September 11, 2013, 4:48 amDanny says:
@Rostyk:
September 11, 2013, 8:23 amYou’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