jQuery ui widget tutorial

来源:互联网 发布:黄磊女儿 知乎 编辑:程序博客网 时间:2024/05/21 08:49

http://bililite.com/blog/understanding-jquery-ui-widgets-a-tutorial/

 

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, likea button or something more complicated like a popup date picker, but in jQuery UI termsit means a class, members of which are associated with HTML elements; things likeDraggable 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 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 tokeep 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 toput it there in the beginning. We also have no way of associatingstate 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 tospecify 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>

This is a test paragraph

The Problems with Associating an Object with a Plugin

But you get into trouble with circularreferences (note that "this.green = new Green($(this))" gives a DOM element a reference to a javascript objectand"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" tokeep track of, and it hides the underlying program logic. As we know, design patterns reflect language weakness.If we areconstantly re-implementing a pattern, we need to abstract it and make it automatic.

Solving the Problem: $.widget

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

You still need to create the constructor of your class, but instead of a real constructor function, you needa 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 likelywill 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 anamespace, like "ns.green" . Unfortunately the namespacing is fake; the plugin is just called $().green(). The constructor functionis $.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.

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 willbe returned; exactly like .html() or .val().


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

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

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 usedfor 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. They do not use the$(element).data('widgetName'); that returns the widget object itself. They store their data in an objectthis.options.Thus:


var Green4 = {
getLevel: function () { return this._getData('level'); }, // note: we could use this.options.level directly, but using the
setLevel: function (x) { // functions gives us more flexibility
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
}
};
$.widget("ui.green4", Green4);
$.ui.green4.getter = "getLevel";

The initial values for the data are stored in the widget's defaultsobject:


$.ui.green4.defaults = {
level: 15,
greenlevels: ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0', '#fff']
};

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>

This is a test paragraph.


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

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

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 inanimations and Ajax. We can create callback functionsthat 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._getData('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 tothe programming framework and the calling program informs the framework that it wants to know about the signal. Events likeclicks and keystrokes work like this, and jQuery allows the widget to create custom events and for the calling program tobind 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 thename 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 passedto the handler. _trigger creates a custom event name of widgetName+type, like green6change(why it doesn't do type+'.'+widgetName the way jQuery expectsis beyond menaming 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 withcallback = 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 datais 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 formfunction(event, data).

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


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]});
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
}
};
$.widget("ui.green5", Green5);
$.ui.green5.getter = "getLevel";
$.ui.green5.defaults = {
level: 15,
greenlevels: ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0', '#fff']
};

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

This is a test paragraph with green level undefined.


// 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 thatincludes lots of useful methods for the mouse. All we need to do is add the $.ui.mouse object to ourwidget prototype:


var Green6 = $.extend({}, $.ui.mouse, {other relevant functions});

And override $.ui.mouse's functions (_mouseStart,_mouseDrag,_mouseStop) to do something useful,and call this._mouseInitin your this._init and this._mouseDestroyin your this.destroy. You also have to incorporate the $.ui.mouse.defaults in your defaults (most easily by doing $.ui.widgetName.defaults = $.extend({}, $.ui.mouse.defaults, {all my other defaults...}).

Let's add some mouse control to our greenerizer:


Green6 = $.extend({}, $.ui.green5.prototype, $.ui.mouse,{ // 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._setData('xStart', e.pageX);
this._setData('levelStart', this._getData('level'));
},
_mouseDrag: function(e){
this.setLevel(this._getData('levelStart') +(e.pageX-this._getData('xStart'))/this._getData('distance'));
}
});
$.widget("ui.green6", Green6);
$.ui.green6.defaults = $.extend({}, $.ui.mouse.defaults, {
level: 15,
greenlevels: ['#000','#010','#020','#030','#040','#050','#060','#070','#080','#090','#0a0','#0b0','#0c0','#0d0','#0e0','#0f0', '#fff'],
distance: 10
});

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

This is a test paragraph with green level undefined.

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

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 眼皮跳了两天了怎么办 右眼皮跳一直跳怎么办 当婚姻走到尽头怎么办 结婚证丢了怎么办离婚 老公有外遇要离婚怎么办 淘宝买到假手机怎么办 在药店买到假药怎么办 玩久了不想工作怎么办 我很懒不想工作怎么办 金寒水冷的八字怎么办 两岁宝宝内八字怎么办 小孩走路脚内八字怎么办 10岁走路内八字怎么办 8岁孩走路内八字怎么办 一岁宝宝足外翻怎么办 2岁宝宝小腿弯怎么办啊 一岁宝宝小腿弯怎么办 一岁小儿o型腿怎么办 两岁宝宝o型腿怎么办 狗狗前腿外八字怎么办 20岁走路内八字怎么办 9岁儿童脚内八字怎么办 5岁宝宝脚内八字怎么办 一岁宝宝内八字怎么办 两人八字合不合怎么办 考到不好的大学怎么办 考的大学不理想怎么办 只考上二本大学怎么办 w7电脑中病毒了怎么办 电脑中病毒了该怎么办 泰迪呼吸急促怎么办啊 狗狗呼吸急促是怎么办 狗狗着凉了呕吐怎么办 狗鼻子流黄鼻涕怎么办 刚出生婴儿睡觉不踏实怎么办 有人溺水后你该怎么办 借钱不还怎么办没欠条 私人欠货款不还怎么办 公司欠货款不还怎么办 两个人离婚一方不同意怎么办 比亚迪l3油耗高怎么办