Intro

The Dojo project is working to build a modern, capable, "webish", and easy to use DHTML toolkit. Part of that effort includes smoothing out many of the sharp edges of the DHTML programming and user experience. On the back of such high-profile success stories such as Oddpost, Google Maps, and Google Suggest, the XMLHTTP object has been getting a lot of attention of late. Sadly, in spite of all the coverage, developers have been on their own when it comes down to solving the usability problems that come along for the ride.

HCI Issues

What usability problems?

Consider that most painful of topics for web application developers: the back button. Web developers armed with some sample code, a decent DOM reference, and a lot of perseverance can build a pretty decent dynamic UI in modern browsers. These UIs doesn't jarringly destroy the user's in-page experience for the most trivial of tasks, like adding an item to a list. When larger portions of an application are mediated in this way, the user naturally has more desire to "go back" to some earlier state if things aren't working out the way they had planned or if the action isn't what they expected. An example might be switching between a view and edit mode in a content editing application.

As high-gloss web applications become the norm, many interactions become intra-page and not inter-page. Programmers looking for creative solutions have chosen XMLHTTP for these scenarios, but unfortunately, XMLHTTP breaks the back button, impairing the user experience. If the back button doesn't function in a way that meets user expectations, it becomes ever easier for the user to lose work or become confused about the state of an application. To assist the user, programmer need a way to capture back-button presses and do something intelligent with them.

If you've used Google Maps and you've tried to send your directions to a friend, you know that not being able to simply copy the URL out of the address bar is significantly confusing at first. Applications that dynamically construct large sections of the UI (like Google Maps) today resort to a link in an intermediate screen that the user can click to return to their current state and then, perhaps, book mark or send to someone else. And this is if and when they consider the "bookmarkability" problem at all. More common is an application that simply refuses to acknowledge that the user might want to pass around a URL to a friend and instead builds some heavyweight and non-standard state serialization mechanism that is more akin to a desktop application's "save as" feature. "Save-as" on the web is book-marking, and usable applications recognize this (even if they don't have great solutions for it today). Regardless of what serialization mechanism is in use, being able to represent the state of the application in a URL (or a marker for serialized state) is a must. This is a hard problem to be sure, and none of the currently available tools provide simple answers.

Introducing dojo.io.bind()

At Dojo, we're committed to making DHTML applications usable, both for authors and for users, and with a lot of help from our friends, particularly Aaron Boodman and Mark Anderson, we have come up with solutions to the usability problems outlined above. We're providing it in a single, easy to use API and a package that requires only two files to function. The dojo.io package provides portable code for XMLHTTP and other, more complicated, transport mechanisms. Additionally, the "transports" that plug into it each provide their own logic to make each of them easier to use. The rest of this article will cover how the XMLHTTP transport from Dojo provides ways around the book-marking and back button problems.

Most of the magic of the dojo.io package is exposed through the bind() method. dojo.io.bind() is a generic asynchronous request API that wraps multiple transport layers (queues of iframes, XMLHTTP, mod_pubsub, LivePage, etc.). Dojo attempts to pick the best available transport for the request at hand, and in the provided package file, only XMLHTTP will ever be chosen since no other transports are rolled in. The API accepts a single anonymous object with known attributes of that object acting as function arguments. To make a request that returns raw text from a URL, you would call bind() like this:

dojo.io.bind({
    url: "http://foo.bar.com/sampleData.txt",
    load: function(type, data, evt){ /*do something w/ the data */ },
    mimetype: "text/plain"
});

That's all there is to it. You provide the location of the data you want to get and a callback function that you'd like to have called when you actually DO get the data. But what about if something goes wrong with the request? Just register an error handler too:

dojo.io.bind({
    url: "http://foo.bar.com/sampleData.txt",
    load: function(type, data, evt){ /*do something w/ the data */ },
    error: function(type, error){ /*do something w/ the error*/ },
    mimetype: "text/plain"
});

It's possible to also register just a single handler that will figure out what kind of event got passed and react accordingly instead of registering separate load and error handlers:

dojo.io.bind({
    url: "http://foo.bar.com/sampleData.txt",
    handle: function(type, data, evt){
        if(type == "load"){
            // do something with the data object
        }else if(type == "error"){
            // here, "data" is our error object
            // respond to the error here
        }else{
            // other types of events might get passed, handle them here
        }
    },
    mimetype: "text/plain"
});

One common idiom for dynamic content loading is (for performance reasons) to request a JavaScript literal string and then evaluate it. That's also baked into bind, just provide a different expected response type with the mimetype argument:

dojo.io.bind({
    url: "http://foo.bar.com/sampleData.js",
    load: function(type, evaldObj){ /* do something */ },
    mimetype: "text/javascript"
});

And if you want to be DARN SURE you're using the XMLHTTP transport, you can specify that too:

dojo.io.bind({
    url: "http://foo.bar.com/sampleData.js",
    load: function(type, evaldObj){ /* do something */ },
    mimetype: "text/plain", // get plain text, don't eval()
    transport: "XMLHTTPTransport"
});

Being a jack-of-all-trades, bind() also supports the submission of forms via a request (with the single caveat that it won't do file upload over XMLHTTP):

dojo.io.bind({
    url: "http://foo.bar.com/processForm.cgi",
    load: function(type, evaldObj){ /* do something */ },
    formNode: document.getElementById("formToSubmit")
});

Phew. Think that about covers the basics. Good thing you weren't planning on implementing all that stuff yourself, right?

So, about that thorny back button...

None of the examples so far would have caused anything but the default (sometimes undesirable) behavior to occur if the user subsequently hit the back button. In a lot of applications with small interactions, it might never transpire that one would want the back button to be intercepted. One could happily build an application using dojo.io.bind() without ever knowing or caring about book-marking or the back button, but if you're reading this, the odds are that you care. So here we go.

First, a note on browser support: all of the following techniques work on IE and Mozilla/Firefox, but Safari is, sadly, a lost cause. That doesn't mean your requests won't send correctly, but it does mean that the forward and back buttons aren't intercept-able. Even when Safari SHOULD support it, implementation bugs in the browser prevent our code from doing the right thing. In addition to not adding iframe document navigation to the forward and back button queues, moving between hash items on a page with forward and back doesn't update window.location.href or provide any other consistent way to determine that either button has been pressed. At this time, it's unknown if Apple is aware of these bugs or has plans to fix them.

For that great majority of browsers that can be supported, catching the back button simply requires registering a callback that will fire when the user hits the back button. Here's the above form submitting example that does a rollback of the form-hide behavior when the back button is clicked:

var sampleFormNode = document.getElementById("formToSubmit");

dojo.io.bind({
    url: "http://foo.bar.com/sampleData.js",
    load: function(type, evaldObj){
        // hide the form when we hear back that it submitted successfully
        sampleFormNode.style.display = "none";
    },
    backButton: function(){
        // ...and then when the user hits "back", re-show the form
        sampleFormNode.style.display = "";
    },
    formNode: sampleFormNode
});

That's it. Just provide that extra backButton argument and your users can now hit the back button and count on your application doing something smarter than destroying their work (with a little help from you, that is).

This naturally leads to the next question: what happens if they hit the forward button after that? Without any intervention on your part, nothing. But, as I'm sure you've come to expect by now, Dojo gives you a hook to handle that situation as well.:

var sampleFormNode = document.getElementById("formToSubmit");

dojo.io.bind({
    url: "http://foo.bar.com/sampleData.js",
    load: function(type, evaldObj){
        // hide the form when we hear back that it submitted successfully
        sampleFormNode.style.display = "none";
    },
    backButton: function(){
        // ...and then when the user hits "back", re-show the form
        sampleFormNode.style.display = "";
    },
    forwardButton: function(){
        // and if they hit "forward" before making another request, this
        // happens:
        sampleFormNode.style.display = "none"; // we don't re-submit
    },
    formNode: sampleFormNode
});

Note that forward button triggers are only fired when the user is linearly progressing between the forward and back buttons. If the user takes an action after going back-back-forward that would fire a new bind() request, the next "forward" will have no callback fired from it since a new branch in the history tree has been created and the old one is likely invalid.

Bookmarking

Bookmarkability is another can of worms for dynamic web applications. With that in mind, dojo.io.bind() gives you one last little bit of control for making things bookmarkable. Simply pass in the value "true" to the flag "changeURL". Here's the example:

dojo.io.bind({
    url: "http://foo.bar.com/sampleData.txt",
    load: function(type, data, evt){ /*do something w/ the data */ },
    changeURL: true,
    mimetype: "text/plain"
});

The effect of this call is that your top-level page will now have a new timestamp hash value appended to the end. Alternately, it's possible to include your own hash value by providing a string instead of "true" to changeURL:

dojo.io.bind({
    url: "http://foo.bar.com/sampleData.txt",
    load: function(type, data, evt){ /*do something w/ the data */ },
    changeURL: "foo,bar,baz",
    mimetype: "text/plain"
});

If the URL of the application had previously been:

http://foo.bar.com/howdy.php

The user's address bar would now show:

http://foo.bar.com/howdy.php#foo,bar,baz

Currently, it's not possible to count on the URL being changed synchronously when the request is fired because of timing issues on Mozilla/Firefox.

Bookmarkability WILL interact with other inter-page navigation using named anchors, but should NOT change the location of the user's scroll position in the viewport. You are advised to test this functionality pretty thoroughly before deploying.

The API for bookmarking is still currently in some flux, but changeURL is not likely to change. Discussions are underway to add a "notify me when we hit this url" feature to bind() that might not even make a call to the server at all. Feedback on this topic is appreciated.

Package Implementation Considerations

If you're not a DHTML hacker or don't want to know how sausage is made, you might want to skip this.

There's a lot of black magic, browser-specific foo, and general hackery rumbling around in this code. On IE, setting the hash of the URL doesn't affect the back-button history, and so an iframe request is required along with a bookmarkable request. On Mozilla, a back-button request is created, but no events fire when back and forward are pressed, so a timer is required to watch the value of location.href. On both browsers, detecting iframe loading requires two queues to differentiate forward from back buttons, and the iframe MUST be part of the initial document (in this case, created w/ document.write) since dynamically created iframes don't affect the history. Likewise, it was somewhat painful to determine what kind of anchor setting would work for creating back-button entries but not change the scroll position in the top-level viewport. It's almost painful to think of how many hours were burned in determining exactly what does and doesn't work. There are, in fact, several areas of the described APIs in which there might be nagging bugs, so please report them if things don't behave as expected.

In short, this is exactly the kind of "don't try this at home" cross-browser hell that turned people off from DHTML in the first place.

Remainders

Usability != Accessibility

The techniques discussed in this article are NOT intended to provide a solution for accessibility concerns. Developers still need to hammer those out for themselves (although Dojo will soon be providing help in that area too). Flaming the author for discussing usability without discussing the negative accessibility impact won't get those tools done any faster, though.

What's In the Package

This article is part of a "profile build" of Dojo that includes a single-file distribution of the Dojo core (for HTML environments), the required "iframe_history.html" file used for generating back-button entries, a test page that demonstrates usage of the API, and this article. Dojo does a LOT more than this package might lead you to suspect, but don't worry, we'll be releasing more soon.

Getting Involved with Dojo

Comitters to Dojo are currently an invite-only group, but that doesn't mean it can't include you. If you'd like to contribute to the future of Dojo and responsive web applications, do something that impress us (either with Dojo, or with JS/DHTML/DSVG in general). Dojo is available under the terms of the AFL 2.1 or BSD licenses. We support your use of Dojo in software that's licensed in other ways, but will not accept differently-licensed patches.

About The Author

Alex Russell is the project lead for Dojo and can be reached at <alex@dojotoolkit.org>. His blog is at: http://alex.dojotoolkit.org