未加星标

Tips for Developing jQuery UI 1.8 Widgets

字体大小 | |
[前端(javascript) 所属分类 前端(javascript) | 发布者 店小二04 | 时间 2016 | 作者 红领巾 ] 0人收藏点击收藏

I'm wrapping up my first jQuery UI widget (see multiselect on GitHub) and thought it would be useful to share some notes I took on the widget factory & widget development in general. I personally found development on the factory to be quite enjoyable; a lot of functionality is available right out the box (custom events, ability to change options after initialization) and idioms you might not otherwise consider, like widget destruction. Furthermore, it imposes a clean object-literal development structure with public and private methods, making it much easier to start a project or maintain other's projects.

Throughout this blog post I will be showing you various ways to modify the jQuery UI dialog widget source to add your own features. I do not actually recommend doing this however, because widgets can just as easily be extended. The concepts are the same though and are easily adaptable to your own project. This post also assumes you are already familiar with the widget factory; if you aren't, be sure to familiarize yourself first.

_init() and _create()

The widget factory automatically fires the _create() and _init() methods during initialization, in that order. At first glance it appears that the effort is duplicated, but there is a sight difference between the two. Because the widget factory protects against multiple instantiations on the same element, _create() will be called a maximum of one time for each widget instance, whereas _init() will be called each time the widget is called without arguments:

$(function(){ // _create() and _init() fire on the first call. $("div").mywidget(); // a widget has already been instantiated on the div, so this time // only _init will fire. $("div").mywidget(); // however, once the widget is destroyed... $("div").mywidget("destroy"); // both _create() and _init() will fire. $("div").mywidget(); });

So how do you know where to place which logic? Use _create to build & inject markup, bind events, etc. Place default functionality in _init() . The dialog widget, for example, provides an autoOpen parameter denoting whether or not the dialog should be open once the widget is initialized; a perfect spot for _init !

// ui.dialog.js - autoOpen is true by default _init: function(){ if ( this.options.autoOpen ) { this.open(); } }

The best way to visualize the difference is to load up Firebug and navigate to the UI dialog demo page. Because the autoOpen parameter is true by default, the dialog is created and opened when the page loads. Close the dialog by clicking on the close link in the toolbar, open Firebug, and attempt to reinitialize the demo by running this in your console: $("#dialog").dialog() . The widget factory knows there is already a dialog instance bound to the #dialog DIV; it just happens to be closed. Therefore, the widget factory will only fire the _init() method, which as you can see above, will simply open the dialog.

_trigger

Custom events can be fired from within your widget by using the internal _trigger method:

// call the trigger method passing in the name of the event to fire, // and optionally an event and data object this._trigger("eventName", eventObj, {});

_trigger is provided by the widget factory, and is not the same as the jQuery trigger() method located in the $.fn namespace (notice the underscore prefix in the widget version). Continuing with the dialog example, developers can execute logic when the dialog opens by either passing in an "open" callback, or later binding to the "dialogopen" event:

// provide an open callback during plugin creation $("#dialog").dialog({ open: function(event, ui){ // do stuff } }); // or bind to the dialogopen event $("#dialog").bind("dialogopen", function(event, ui){ // do stuff });

When I was trying to integrate similar functionality into multiselect, I scoured the dialog's source code looking for a trigger on the "dialogopen" event to see how it was done. Said trigger does not exist. Black magic, I thought, but as it turns out there is a call to _trigger("open") inside the dialog's "open" method. Success!

// ui.dialog.js open: function(){ var self = this; // open logic here // event is fired self._trigger('open'); }

When self._trigger("eventName") is called:

The widget factory automatically prepends the widget's name to the name of the event you want to trigger - and fires the event. If there is a function with the name of the event you're trying to trigger inside the options object , that function will be called as well. A number of other things go down - like copying the original event object if it exists - but for developers, the points above are the most important parts to remember. Preventing default actions with _trigger

There are a number of events in jQuery UI where, if you bind to them and return false , the default behavior will not fire. The "select" event in the Autocomplete widget, for example, fires when an item is selected from the menu. From the UI docs:

The default action of select is to replace the text field's value with the value of the selected item. Canceling this event prevents the value from being updated, but does not prevent the menu from closing.

Applying this to the dialog widget, one might expect that something like this will prevent a dialog from opening:

// no workie $("#dialog").bind("dialogopen", function(){ return false; });

This is not supported with the open event, nor should it to be, because users expect this type of event to fire after the dialog has opened. So let's modify the source of the widget and add our own custom "beforeopen" event, which will fire immediately once the open method is called. Before the logic to actually display the dialog, and before the open event is fired:

// ui.dialog.js open: function(){ // bail out of the open method if the returned value from the // beforeopen event is false. if(this._trigger('beforeopen') === false){ return; } // continue rest of dialog open logic. }

Now, we can bind to this new beforeopen event and prevent the dialog from opening simply by returning false:

$("#dialog").bind("dialogbeforeopen", function(){ return false; }); // or $("#dialog").dialog({ beforeopen: function(){ return false; } });

Huzzah! On the same note, event callbacks take two optional parameters: the event object and an options object. If an event was triggered by another event - like the "close" event being triggered by clicking on the close icon in the dialog's title bar - the original click event has not been lost. In this context, event.type equals "dialogclose", but the "click" event is accessible under the event.originalEvent namespace:

$("#dialog").dialog({ close: function(event, ui){ alert(event.type); // => "dialogClose" // when fired programatically: $("#dialog").dialog("close"): alert(typeof event.originalEvent); // => "undefined" // but when called with the esc key or clicking on the // close icon: alert(typeof event.originalEvent); // => "object" alert(event.originalEvent.type); // => "click" } }); Accessing other widget instances

Let's say you want to ensure that only one dialog can be open at any given time; that is, when a dialog's open method is called, all other open dialog instances should be closed. Unfortunately the widget factory does not hold an internal cache of widget instances, but this feature is trivial to implement none the less.

First, it is important to understand how a DOM element is related to the widget it was initialized it on. Most (if not all widgets) follow this pattern:

You, the developer, queries the DOM for an element and calls the widget you want to initiate. The widget generates new markup based on that element and injects it into the DOM somewhere. The entire widget instance is then stored in is the element's data() cache. Optionally, if it makes sense, the widget hides the original element, seemingly transforming the old element into a new widget. Let me hit you with that once more in case you missed it: the entire widget instance is stored in the element's data() cache. If you were to examine $("#element").data() in firebug, you would see an object with the name of the widget you created on that element.

Once $("#mydiv").dialog() is called, new markup is generated and injected into the DOM, and the original mydiv element is transformed into the content pane of the dialog. Even through the mydiv element is now apart of the widget, it continues to be our link between the developer and the dialog instance:

// create a new dialog $("#mydiv").dialog(); // #mydiv is still in the DOM, but is hidden. // it is our link back to the widget. so, to interact with // the widget, we call a method on the original element: $("#mydiv").dialog("open"); // or cache it in a var var foo = $("#mydiv").dialog(); foo.dialog("open");

This explains the relationship of a DOM element to it's dialog instance, but in order to close all other dialogs, this relationship needs to be reversed. All instances have to be found first, and then we access the underlying DOM element and perform the close method call.

You may be tempted to hack the dialog's open method to find every element and aimlessly fire the dialog close method:

// omg no - ui.dialog.js open: function(){ $("*").dialog("close"); // continue with open code }

...which would work, but there is a better way. Actually, there are two three better ways:

Method 1: store the original element in the widget's data cache

I first came across this approach in the Filament Group's select widget , so here's a quick shout-out to them. Since each dialog instance is stored in the element's data cache, why not store the element inside the widget's data cache? This way we can easily query the DOM for all dialog widgets, access the underlying DOM element, and then fire the close method on those elements. We'll start by modifying the _create method to store the DOM element in the new widget:

// ui.dialog.js _create: function(){ // all the original create code here. // the dialog's markup is generated and saved into // this 'uiDialog' variable. var uiDialog = '<div class="ui-dialog"> ... </div>'; // store the original element (this.element) inside the // widget's data store. uiDialog.data("originalelement", this.element); }

Next, at the top of the "open" method, query the DOM for all dialog widgets (DIV elements with the class ui-dialog) and filter out the current instance. It is necessary to exclude the current instance because otherwise, given two dialogs A and B where A is closed and B is open, opening A would trigger the close event for both A and B, when the event should only be fired on B. Finally, loop through the matching ui-dialog DIV's and grab the element it was originally created with out of the data store. If the dialog is open, determined by calling the dialog("isOpen") method, then close it:

// ui.dialog.js open: function(){ $("div.ui-dialog").not(this.uiDialog).each(function(){ var el = $(this).data("originalelement"); if(el.dialog("isOpen")){ el.dialog("close"); } }); // rest of open code } Method 2: maintain your own cache This method is certainly easier to understand, and involves extending the dialog widget to add a public property called "instances". When you define a widget with the $.widget("namespace.widgetname") syntax, the widget factory instantiates the widget "class" inside $[ namespace ] . All jQuery UI widgets exist in the $.ui namespace, which is reserved for official widgets, so make sure you change this to something unique. Namespaces are used internally for organizational purposes; they do not allow you to create multiple widgets with the same name.

With this in mind, $.ui.dialog can easily be extended to include an instances property, which will start out as an empty array. Each time a dialog in initialized, the associated DOM element will be pushed into it, providing a clear path to all instances. On the token, each instance will be removed from the array on destruction.

Towards the bottom of the ui.dialog.js file, extend $.ui.dialog to include an instances property:

// ui.dialog.js (function($){ $.widget("ui.dialog", { // core of widget code here }); // the dialog has some other code here // extend $.ui.dialog, adding a public "instances" property $.extend($.ui.dialog, { instances: [] }); }(jQuery));

Next, push the original DOM element onto the $.ui.dialog.instances array during initialization:

// ui.dialog.js _create: function(){ $.ui.dialog.instances.push(this.element); // rest of _create code here }

The advantage of this pattern is clear: all instances of the widget are accessible publicly AND from within the widget itself. It is trivial to access all elements that have a particular widget bound to them from outside your widget:

// this line becomes valid from both inside and outside the widget // as soon as ui.dialog.js is loaded into your page: alert("There are currently " + $.ui.dialog.instances.length + " dialog instances.");

Next, in the open method of the dialog, use $.grep to remove the current instance from $.ui.dialog.instances , saving these "other" instances into the variable otherInstances . Loop through this new array, check if the dialog associated with it is open, and if so, call the close method:

// ui.dialog.js open: function(){ // the DOM element associated with this instance var element = this.element; // go through each element in the array, returning a // new array of instances excluding the current one var otherInstances = $.grep($.ui.dialog.instances, function(elem){ return elem !== element; }); $.each(otherInstances, function(){ var $this = $(this); if( $this.dialog("isOpen") ){ $this.dialog("close"); } }); // rest of open code }

The final step involves cleaning up after ourselves. Each time an instance is destroyed it needs to be removed from the array, trivial enough to implement with jQuery's $.inArray method and javascript's Array.splice method:

// ui.dialog.js destroy: function(){ // the DOM element associated with this instance var element = this.element; // the index, or location of this instance in the instances array var position = $.inArray(element, $.ui.dialog.instances); // if this instance was found, splice it off if(position > -1){ $.ui.dialog.instances.splice(position, 1); } // continue destroy logic }

Update 04/22/2010: Adam Sontag pointed out a third method:

Method 3: widget pseudo-selector

In the #jquery IRC channel, Adam Sontag of the yayQuery fame noted an undocumented feature of the widget factory: automatic pseudo selector generation for all widgets. With this, it is super simple to query the DOM for all widgets of a certain type:

// gimme all dialogs $(":ui-dialog");

The selector above returns an object of DOM elements each instance was created on. No need to maintain your own cache or store the DOM element inside each widget. Closing all other open dialog instances using this pseudo selector is trivial:

// ui.dialog.js open: function(){ // close all open dialogs, excluding this instance $(":ui-dialog").not(this.element).each(function(){ var $this = $(this); if($this.dialog("isOpen")){ $this.dialog("close"); } }); // rest of open code here }

Nice and easy. Choose whichever you think works best for you. Method #2 gives you immediate access to a public array of instances and does not rely on querying the DOM; however, method #3 could not be any simpler.

Widget destruction & progressive enhancement

If your widget progressively enhances an element - that is, it transforms something that works into something that works better and with a candy coating - always keep in the back of your mind what will happen if it is later destroyed. I cannot think of an appropriate example with the dialog widget, but multiselect fits the bill perfectly. It works like this:

A standard HTML multiple select box exists in the DOM. $("select").multiselect() is called which hides the select box and injects new markup into the DOM. Users now interact with the widget using checkboxes instead of option tags.

Just as you would ensure that pre-selected option tags are checked by default when the widget is created, you also want to think in reverse. If a user checks 5 boxes and the widget is then destroyed, one would expect the same 5 option to also be selected. Therefore, when a checkbox receives the checked="checked" attribute, the associated option tag also receives the selected="selected" attribute, even though it is hidden from view . Therefore, the widget can be destroyed and re-created without losing any action the user performed.

Honestly, I cannot think of a legitimate use case where a widget is destroyed. The widget factory supports it, however, so you should too.

Resources

For more widget goodness:

See a sample widget template using most of the conventions outlined above. Widget factory 1.7.2 to 1.8 upgrade guide and sample templates Widget factory development & planning documentation Widget factory developers guide More blog posts about widget development

If you have any feedback, questions, or other useful tips, please leave them in the comments section below.

本文前端(javascript)相关术语:javascript是什么意思 javascript下载 javascript权威指南 javascript基础教程 javascript 正则表达式 javascript设计模式 javascript高级程序设计 精通javascript javascript教程

主题: jQueryJavaScriptHTMLGitHubJavaGitFila
分页:12
转载请注明
本文标题:Tips for Developing jQuery UI 1.8 Widgets
本站链接:http://www.codesec.net/view/482590.html
分享请点击:


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 前端(javascript) | 评论(0) | 阅读(34)