Wednesday, November 6, 2013

jQuery UI: Dialogs and URLs

The jQuery UI dialog widget will display itself automatically when created. That is, unless you set the autoOpen option to false. Developers can choose the approach to take in how dialogs are created, and subsequently opened. For example, the default mode let's us work with short-lived dialog widgets. They're displayed when instantiated, destroyed when closed. This life-cycle works fine for the most part and is very resource-friendly. The alternative is to created your dialog widgets upfront, and simply call the open() and close() methods. What's common to both approaches is that they're reacting to some using event. It could be something has finished loading, or the more common case, the user has clicked something can in response, we want a dialog displayed. What about responding to URL changes?

Imaging we're developing some application that uses a few common dialogs, all throughout the application. Maybe there is a global toolbar, visible on every page, with "about" and "settings" links. When these are clicked, we want their corresponding dialogs opened. Since these dialogs are used all over the place, we'll assume that the right approach is to create these dialogs upfront, so they're ready to be opened in response to these events.

For these links in our toolbar, we could choose to simply implement a click event handler that opens the appropriate dialog. Or, alternatively, we could just assign the link a hash-bang URL, and not directly attach any event handlers to the link. This would mean that when the URL hash changes, some logic would have to locate the dialog based on the URL, and open it. Why would we do such a thing? Well, it's beneficial to both the user and the developer. The user has a URL that they can work with, which is always an intuitive bonus. The developer has a single point in the code to manage the dialog display. So how would we go about implementing such a capability?

This demo takes the hypothetical "about" and "settings" links, and assigns them URLs. When then listen for hash change events, and find the appropriate dialog to display based on the URL content. It sounds easy enough, and it is, but there are a few subtleties to look out for. Like what should happen to the URL when the dialog is closed by the user? Let's step through the code.

// A regex that looks for a dialog ID in the URL.
var dialogRe = /^#!dialog\/(\w+)$/;
    
// Create two dialog widgets.
$( "#about, #settings" ).dialog({
    autoOpen: false,
    modal: true
});

Here, we're creating a regular expression used to match a dialog ID in a URL. Notice how we're explicitly marking "dialog" in the URL, so there's never any confusion about what it represents. We then create our two dialog widgets that don't open till we call the open() method.

$( ":ui-dialog" ).dialog( "close" );

We're now into the hashchange event handler. The first thing we do here is close all dialog widgets. The :ui-dialog selector will only match dialog widgets, and so it's safe to call dialog methods on the results.

var match = location.hash.match( dialogRe ),
    $dialog;

Now we look for a dialog in the URL, using the regex we constructed earlier. If there's no match, we simply return since there's no dialog for us to display. If there is a match, on the other hand, we have to look up the dialog and display it.

$dialog = $( "#" + match[ 1 ] )
           
$dialog.dialog( "option", "close", function( e ) {
    if ( $( e.currentTarget ).is( ".ui-dialog-titlebar-close" ) ) {
        history.back();
    }
});
            
$dialog.dialog( "open" );

Here, we can use the captured ID from the URL, and look up the dialog widget. The next thing we do is attach a callback function to the dialog close event. The reason for doing so, is to change the URL to the previous value, using histroy.back(). However, we only want to do this if the event is coming from the dialog close button. Finally, we can open the dialog. And that's it for the hashchange handler. There's just one more thing we have to do.

$( window ).trigger( "hashchange" );

The last thing we do when the DOM is ready is explicitly trigger the hashchange event. The reason we do this is so that our logic will still work as expected, even when the URL with a dialog hash bang is loaded initially.