Tuesday, October 29, 2013

jQuery UI: Quick and Dirty Dailog Bar

A task bar in traditional Windows environments are where inactive widgets go. It's a good idea, if you have several dialogs used by your application, and no consistent place to put them all, to have just such a task bar in place. This is where the inactive windows go — all in one place, and easy for the user to find. Enterprise-grade JavaScript widget kits offer such task bar components, but they're not found in jQuery UI. So, let's extend the base widget and implement a quick and dirty dialog bar.

Here is a sample implementation of a dialogbar widget. And this is what it looks like with two dialogs inside.


Now let's step through some of the more important bits of code that makes this widget work. The goal of the dialogbar widget is to take a set of dialog widgets and listen to their close events. When the close event fires, we want the dialogbar to keep track of that dialog for the user by displaying the dialog title. When the dialog title is clicked, the actual dialog needs to be restored.

// Classes applied to the dialogbar widget element.
var classes = [
    "ui-dialogbar",
    "ui-widget",
    "ui-state-default",
    "ui-corner-top"        
];

Here we have the classes we want applied to the widget element. We'll ultimately use the addClass() function for this job, after joining this array together as a single string. You might be wondering why store this array of strings when we only care about adding them to the element once, and as a single string? Well, an array of strings laid out this way makes for much more readable code, especially if we want to introduce a new class later on. You'll also notice that the classes array isn't stored anywhere inside the actual widget definition. This is also on purpose — the array is accessible through the closure scope, and that way we need only define it once.

// Our options. We can tell the dialogbar which dialog widgets
// we care about.
options: {
    dialogs: $()
},

At this point, we're into our dialogbar widget definition. The first thing we want to tackle is adding any options to the widget. In the case of our widget, we want an option called dialogs. These are the dialogs we're interested in monitoring for close events, then keeping track of so they can be opened later on. You'll notice that the default value chosen for this an empty jQuery selector — or just a jQuery constructor, with no selector string. This is the same behavior as a developer using this widget, and passing in a selector that matches zero elements — completely valid and sane default functionality.

// Listen to dialog close events, but only for dialog
// widgets in the "dialogs" option for this widget.
this._on( this.options.dialogs, {
    "dialogclose": "_ondialogclose"    
});

// Listen for click events inside this widget.
this._on( this.element, {
    "click a": "_onclick"
});

Here we are in the widget constructor, just after applying our widget classes to the element — it's time now to setup some event handlers. We'll use the _on() method that comes to us from the base widget class. What's really neat about this method is that when the widget is eventually destroyed, the cleanup activities, with regard to unbinding event handlers, is performed for us. Also, as you can see here, we can use this method to listen to events outside of this widget — like dialogclose. The first argument is the element we're listening to. Our code uses this.options.dialogs for listening to close events. If this first argument is omitted, the element is assumed to be this.element. However, be careful using this implicit form. While it could make perfect sense to yourself, others that have to read your code may not be so fortunate. And so we've gone with the explicit form in our dialogbar code.

// Get the title of the closed dialog, and it's index relative to the
// "dialogs" option of this widget.
var title = $( e.currentTarget ).dialog( "option", "title" ),
    index = this.options.dialogs.index( e.currentTarget );

// Add the dialog link to the dialog bar.
this._hoverable( $( "<a/>" ).attr( "href", "#" )
           .data( "dialog", index )
           .text( title )
           .appendTo( this.element ));

This is our _ondialogclose() handler, responding to a dialogclose event. The first thing this code does is fetch the dialog title. We'll be using this string for displaying the dialog within the dialogbar. The second thing we do is figure out the index of the dialog being closed relative to the dialogs option. For example, the second dialog in the jQuery object passed to the dialogs option could be closed, in this case the index would be 1. This indexing behavior is at the heart of our widget since it's job requires that it open the dialog again, once a link is clicked. To do that, we need to look up the correct dialog instance somehow. There are a number of approaches we could have gone with here, but the indexed approach seems the most effective and straightforward. We already have our list of candidate dialog instances in the dialogs option, and so an index value will do nicely. And now we need to tie this index value to the link somehow.

The link we're creating here uses the dialog data key to store this index. Once created, we're simply adding the link to the dialogbar element, which is a div. We have some styles already applied via classes that will assist with the presentation of the link. We're also using the _hoverable() method which takes care of adding and removing the appropriate class when the user hovers over the link.

// We don't care about the default link behavior.
e.preventDefault();

// Get the clicked link, and the index data needed to open
// the associated dialog.
var $link = $( e.currentTarget ),
    index = $link.data( "dialog" );

// Open the dialog.
this.options.dialogs.eq( index ).dialog( "open" );

// Remove the link.
$link.removeData( "dialog" )
     .remove();

Finally, this is how we respond to a dialog title link being clicked within our dialogbar widget. The first thing we do is cancel the default click event behavior, since all we care to do here is open up a dialog. Next, we store the link element in $link since we'll need it again. Then we get the dialog index using the data attribute. Once we have the dialog index, we can look up the specific instance in the dialogs option, which is simply a jQuery object. Now we can terminate the link from the DOM, taking special care to remove the data we attached to the element first.