Understanding jQuery UI widgets: A tutorial
This page is obsolete; current versions are on my github pages at github.bililite.com/understanding-widgets.html. This page is being kept here for historical purposes.
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.
Dan Wellman has another tutorial that you may find helpful.
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).
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); },
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 "ns.green
" . Unfortunately the namespacing is fake; the plugin is just called $().green()
. The constructor function
is $.ns.green
, but you never use that, so you might as well use the "official" namespace of "ui
". But defining the widget couldn't be easier:
$.widget("ui.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); },
off: function() {
this.element.css({background: 'none'});
this.destroy(); // use the predefined function
},
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("ui.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>
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("ui.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("ui.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 naming events this way has been the subject of some discussion), sets type+'.'+widgetName
the way jQuery expects
is beyond meevent.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)
.
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("ui.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 ('ui.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({}, $.ui.green5.prototype, { // leave the old Green5 alone; create a new object
_init: function(){
$.ui.green5.prototype._init.call(this); // call the original function
this._mouseInit(); // start up the mouse handling
},
destroy: function(){
this._mouseDestroy();
$.ui.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("ui.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 ought to be abstracted out
into its own method, something like
$.ui.green5.subclass("green6", $.ui.mouse, {mouseStart: function(){}, mouseDrag: function(){}})
but that's a topic for another day.
Essential jQuery Plugin Patterns - Smashing Coding says:
[…] “Understanding jQuery UI Widgets: A Tutorial,” Hacking at 0300 […]
October 11, 2011, 7:33 amEssential jQuery Plugin Patterns@smashing | seo???? says:
[…] “Understanding jQuery UI Widgets: A Tutorial,” Hacking at 0300 […]
October 11, 2011, 7:55 amEssential jQuery Plugin Patterns | Blogs – NG Outsourcing says:
[…] “Understanding jQuery UI Widgets: A Tutorial,” Hacking at 0300 […]
October 11, 2011, 2:14 pmEssential jQuery Plugin Patterns | IdentityNepal.com says:
[…] “Understanding jQuery UI Widgets: A Tutorial,” Hacking at 0300 […]
October 11, 2011, 3:34 pmEssential jQuery Plugin Patterns | Ricky Noel Diancin Jr. Webmaster | Web Designer | Wordpress Expert says:
[…] “Understanding jQuery UI Widgets: A Tutorial,” Hacking at 0300 […]
October 11, 2011, 4:12 pmEssential jQuery Plugin Patterns | Testing themes says:
[…] “Understanding jQuery UI Widgets: A Tutorial,” Hacking at 0300 […]
October 11, 2011, 8:30 pmEssential jQuery Plugin Patterns – Just a blog :) says:
[…] “Understanding jQuery UI Widgets: A Tutorial,” Hacking at 0300 […]
October 12, 2011, 6:07 ambaidu ways says:
hey, this power be bit offtopic, but i am hosting my place on hostgator and they wishes interrupt my hosting in 4days, so i would like to ask you which hosting do you purpose or recommend?
October 22, 2011, 9:11 amDanny says:
@baidu:
October 22, 2011, 8:09 pmI use 1and1.com, on their cheapest plan. It’s old-line LAMP (linux-apache-php-mysql) only, and not updated very frequently, and down for an hour once or twice a week. In short, it’s worth everything I pay for it and I have no complaints, but it’s nothing fancy.
–Danny
Mark William says:
Very very important tutorial.
November 3, 2011, 5:20 amThank you somuch for this wonderful post.
Cordyceps says:
That is very nice what I did not undestand is how do you call setLevels?
November 8, 2011, 3:08 pmCan setlevels be called externally?
Danny says:
@Cordyceps:
November 8, 2011, 3:41 pmThis may have been unclear, but the way to call all methods of UI widgets is by calling the plugin with the name of the method:
$('.target').green6('setLevel', 7);
The UI documentation goes into using widgets in more detail.
–Danny
Chemises homme says:
Thank dany but,
December 8, 2011, 4:25 amwhy you don’t use the toggle function ?
Danny says:
@Chemises homme (I assume you are a real person who designed that site):
December 8, 2011, 6:38 pmYes, for turning the color off and on in real life you would use a simple toggle. That was simply an example of how to create a widget; the example needed to be simple enough to not “get in the way” of the explanation. Actual widgets would be far more complex and need to store state and respond to events.
–Danny
Rafi says:
Professionally developed and ready to use jQuery widgets.
December 8, 2011, 9:41 pmDanny says:
@Rafi:
December 9, 2011, 12:29 pmYour comment is just an advertisement for your own set of widgets, which as far as I can tell to not even use jQuery UI. But they look cool and may give people ideas for their own widgets, so I’ll let it slide.
brice says:
there is an error in function green2 ( first version )
should be this :
if (!this.green) this.green = new Green($(this)); // associate our state-keeping object with the element
instead of this :
if (!this.green) this.green = new Green(this); // associate our state-keeping object with the element
thanks for the tuto ! very interesting !
May 7, 2012, 3:47 amDanny says:
@brice:
May 7, 2012, 8:38 amYou are correct. I have corrected it.
–Danny
A Step-by-step Guide to Code your first jQuery Plugin (Part 1 – Basic Resources and Tutorials) | Bold The Sign Blog says:
[…] Before jump right into the code, there are some basic things that you need to know, you can learn the jQuery Plugin authoring basics, practices, and common pitfalls in developing the plugin here […]
May 10, 2012, 4:36 amWrite your own jQuery widget says:
[…] http://bililite.com/blog/understanding-jquery-ui-widgets-a-tutorial/ […]
July 4, 2012, 5:08 amChà Thanh says:
Thanks! This post is very helpful for me.
July 16, 2012, 9:59 pmnyc says:
Still assimilating this well-crafted presentation.
Can you tell us why widget destroy is not underscored?
July 29, 2012, 12:35 amDanny says:
@nyc:
July 31, 2012, 8:00 amThe underscored methods like
_init
, are private; they cannot be called with$(...).widget('methodname')
. You may want to remove a widget with all its data, so you can call$(...).widget('destroy')
.You never call
_init
directly; you create a widget with$(...).widget(options)
.Hope this helps.
–Danny
nyc says:
Got it, thanks – in fact I saw an example of client code doing just that since I posted.
Btw, “sick” is home-growed American slang as far as I can tell :)
August 2, 2012, 10:04 pmTrev says:
Thanks for writing this up and taking the time to include working examples.
August 19, 2012, 12:41 ammoises gil says:
Dear Sirs
I would like to know how can I insert and work with the JQuery UI software.
August 19, 2012, 6:11 amYour help will be appreciate
rgds
Danny says:
@Moises Gil:
August 19, 2012, 8:27 amI can’t offer introductory tutorials, but there are lots of them out there: http://www.bing.com/search?q=jquery+ui+tutorial.
–Danny
F1.net » Blog Archive » jQuery Tutorials says:
[…] http://bililite.com/blog/understanding-jquery-ui-widgets-a-tutorial/ […]
September 14, 2012, 12:58 amHow to Make JQueryUI Widgets says:
[…] Understanding jQuery UI widgets: A tutorial […]
October 9, 2012, 11:18 amjqueryheaven says:
Actually, this is a awesome starter tutorial, and introduce some basics of jquery
November 22, 2012, 2:23 pmJames Sandberg says:
Great article. thanks for putting in such efforts for us all.
November 24, 2012, 12:53 pmDerek says:
Do tools like JSHint freak out when you call methods that don’t immediately exist?
Such as this example in Green5:
`this._trigger(‘done’);`
December 13, 2012, 10:45 amDanny says:
@Derek:
December 13, 2012, 11:31 amI don’t know; I’ve never done it. It should be OK, since the
_trigger
method is defined on the widget, so if JSHint is smart enough to know whatthis
is, it should know what methods are defined.–Danny
Essential jQuery Plugin Patterns - Goodfav Howto says:
[…] “Understanding jQuery UI Widgets: A Tutorial,†Hacking at 0300 […]
March 4, 2013, 6:33 pmHow to handle events in jQuery UI widgets | BlogoSfera says:
[…] trying to write a jQuery widget following the model given here. Here is a snapshot of the […]
July 16, 2013, 4:02 amEssential jQuery Plugin Patterns | Smashing Magazine says:
[…] “Understanding jQuery UI Widgets: A Tutorial,†Hacking at 0300 […]
April 15, 2014, 4:06 am25个å¦ä¹ jQuery UIçš„æ•™ç¨‹èµ„æº | 应酷爱网页设计 says:
[…] 了解jQueryçš„UI部件 – 教程 […]
April 18, 2014, 12:57 amWidget encapsulation in JavaScript frameworks | Zorc Answers says:
[…] Refer jQuery UI Developer Guide, Tips for Developing jQuery UI 1.8 Widgets , Understanding jQuery UI widgets: A tutorial […]
December 11, 2014, 6:38 amLearning JavaScript Design Patterns | Maki's iNote says:
[…] “Understanding jQuery UI Widgets: A Tutorial,†Hacking at 0300 […]
December 27, 2014, 5:28 am