← all posts

Tips for Developing jQuery UI 1.8 Widgets

Thursday, April 22, 2010

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:

  1. The widget factory automatically prepends the widget's name to the name of the event you want to trigger - and fires the event.
  2. 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:

  1. You, the developer, queries the DOM for an element and calls the widget you want to initiate.
  2. 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.
  3. 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 twothree 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:

  1. A standard HTML multiple select box exists in the DOM.
  2. $("select").multiselect() is called which hides the select box and injects new markup into the DOM.
  3. 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:

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

comments powered by Disqus