.. title:: dojo.io.bind(): baking usability into XMLHTTP

.. raw:: html
   :file: ../website/rest_template/header.html

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
------------------------------------

.. FIXME: aaron wants just a listener that he can register when a particular
.. hash that he's intersted in gets called out to.

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

.. raw:: html
   :file: ../website/docs/sidebar.html

dojo.io.bind(): baking usability into XMLHTTP
=============================================

:Author: Alex Russell
:Version: 0.3
:Copyright: Dojo Foundation, 2005
:Date: 2005/03/11

.. contents::

.. raw:: html
   :file: ../website/docs/menu.html

.. raw:: html
   :file: ../website/rest_template/footer.html

.. vim:ts=4:et:tw=0:shiftwidth=4:
