Make the browser back button work the way web users expect it to in Ajax applications. Some Ajax applications change the behavior of the browser back button in a way that is unacceptable to users who are big fans of the back and forward buttons. For example, if you use the back button while reading your Google mail messages, you might be greeted by a blank white page that displays the text "Loading..." without anything else happening (Gmail solves this problem by providing its own "Back to Inbox" navigation control within the Gmail application). What is the usual role of a browser back button? The back button jumps you back through your browser's page history. In typical web applications, when the user clicks on a hyperlink, the data is updated by building a whole new page. A new page represents a new browser history entry.
But Ajax applications don't create new pages. Rather, they update content within an existing page. And therein lies the problem: when users press the back button they jump back to the previously loaded pageand often out of the Ajax application. How can you tell the browser to add a new entry to the browser history in an Ajax application? It depends on the browser. The DOM window object has a property, location, that lets you set the URL to display on the user's screen. However, this normally means the user jumps to a new page, which doesn't help us. Is there any way to change the location URL without causing a page reload? It turns out there is. You've seen URLs that look like this: http://www.example.xyz/frobnitz#xyz. The hash mark (#) is called a fragment identifier (or, more commonly, an anchor hash). The characters that appear after the hash point to a marker inside the current document. (You'd see the target of the above link written as <a name="xyz">.) In the Firefox and Opera browsers, if you change the fragment identifier (hash) of window.location in JavaScript, a new history entry is created. When you hit the back button, the history pulls the previous page from the browser cache. This works in Internet Explorer, tooalmost. Internet Explorer adds a history entry but doesn't cache the page data, so when the user presses the back button the data on the page doesn't change. So, scratch window.location. What else updates the browser's history? Changing the contents of an iframe adds a history entry, and it works across the major browsers (this hack, however, doesn't work with Safari 2.0). That's the approach we'll use in this hack.
This hack is contained in a single JavaScript file, bbfix.js, so it's easy to plug into existing projects. Ideally, you should be able to drop this file into an existing Ajax application and, with minimal fuss, get a working back button.
Before looking inside this hack, let's look at how it's used within a program. A Very Simple Ajax ProgramThis simple Ajax program, uptime.html, includes a form holding a single button. Press the button, and a JavaScript function fires off an XMLHttpRequest request for the web server's uptime value. The value is displayed on the page. Press the button again, and the uptime value is updated.
Here's the web page code: <HEAD> <TITLE>Ajax Back Button Hack</TITLE> <script language="javascript" src="/books/4/254/1/html/2//bbfix/xhr.js"></script> <script language="javascript" src="/books/4/254/1/html/2//bbfix/bbfix.js"></script> <script language="javascript" type="text/javascript"> function onClick_btnGetUptime ( ) { var httpreq = getHTTPObject( ); httpreq.open("POST", "/cgi-bin/bbfix/uptime.cgi", true); httpreq.onreadystatechange = function ( ) { if (httpreq.readyState == 4) { //Update the uptime results. var content = document.getElementById("divUptime"); content.innerHTML = httpreq.responseText; //Store the new contents in the cache. bb_save_state ( ); } } //Opera needs "", not null, as a send( ) //parameter, else it fails. httpreq.send (""); } </script> </HEAD> <BODY onload="bb_init('divBody', true);"> <div > <b>Ajax Back Button Hack</b> <div > </div> <form > <input type="button" value="Get Uptime" onClick="onClick_btnGetUptime( );" > </form> </div> <!-- Invisible IFRAME required by bb_fix module: --> <iframe name="bbFrame1" width="0" height="0" style="visibility: hidden; inline: none;" > </iframe> <!-- bbfix.js inserts debugging info here, if enabled: --> <div > </div> </BODY> </HTML> Using bbfix.js requires just five steps:
Inside the HackThis hack works by detecting when the back button is pressed, and then rolling back the web page to a previous state. Within your Ajax app, you determine these "rollback" points by calling bb_save_state( ). Figure 9-1 shows what the web page looks like after the user clicks the Get Uptime button three times. Each button press gets a new uptime value from the server. The number always increases (such is the nature of time). Figure 9-1. The web page after three updatesThe bb_save_state( ) function stores a portion of the current web page into a JavaScript array. A global variable keeps track of the current index into that array. After saving the current state, the function then updates the contents of the hidden iframe bbFrame1. Updating the iframe is the hackish code piece that later lets us know when the back button has been pressed. The hidden iframe is updated by calling a very simple server-side script called count.cgi.[2] The sole function of this server script is to store the current array index. For array index 4, it will place this into the iframe:
<HTML> <HEAD> </HEAD> <BODY onload='parent.bb_done_loading( );'> <div >4</div> </BODY> </HTML> The debugging information at the bottom of the page in Figure 9-1 shows that the cache has been updated via bb_save_state( ) four times (the initial state of the page is stored in the first cache entry). The code calls the bb_init( ) function once, when the Ajax page is first loaded. Its most important job is to start up an interval timer. This timer fires off once a second, calling the function bb_check_state( ). This function detects if the back button has been pressed. When the back button is pressed, the browser automatically rolls back the contents of the iframe to its previous state. The browser caches this state in its browser history. (If all browser versions consistently stored the rest of the page as well, there'd be no need for this hack.) When the interval timer fires, bb_check_state( ) looks at the index value stored in the iframe's tag. If it's changed, you know the back button has been pressed. You can use the contents of our own cache array to update the Ajax page. Figure 9-2 shows uptime.html after the back button was pressed. Notice that the time is earlier than in Figure 9-1, proof that this data came from our cache, not the server. The debugging information at the bottom of the page bears this out. An iframe change was detected by bb_check_state( ), and the divBody tag was updated with the cached content. Figure 9-2. The page after the user has pressed the back buttonHere is the code from bbfix.js that makes this work: var bb_count = 0; var bb_curr_idx = ""; var bb_cache = new Array; var bb_debug = false; var bb_iframe_script = "/cgi-bin/bbfix/count.cgi"; var bb_iframe_loaded = false; var bb_target_div = ""; //If debug is enabled via bb_init( ), then //we append some data to the divTrail //element. function bb_debug_update (str) { if (bb_debug) { var divBBDebug = document.getElementById("divBBDebug"); divBBDebug.innerHTML = divBBDebug.innerHTML + "<br>" + str; } } //Run from the interval timer (once a second), //this function reads a cache index value //stored in the DIV element of the child IFRAME. // //If this extracted cache index differs from the //current cache index, then the back button was //pressed. In this case, we pull the corresponding //data from the cache and update the page. function bb_check_state ( ) { if (bb_iframe_loaded == false) { return; } var doc = window.frames['bbFrame1'].document; var new_idx = doc.getElementById('divFrameCount').innerHTML; if (new_idx != bb_curr_idx) { var debug_msg = "IFRAME changed. Was " + bb_curr_idx + ", now " + new_idx; //Pull a previous state from the cache (if it exists). if (bb_cache[new_idx]) { var divBody = document.getElementById("divBody"); divBody.innerHTML = bb_cache[new_idx]; debug_msg += " [pulled " + new_idx + " from cache]"; } bb_curr_idx = new_idx; bb_debug_update (debug_msg); } } //Called by child IFRAME. function bb_done_loading ( ) { bb_iframe_loaded = true; } //Update the hidden IFRAME. function bb_loadframe ( ) { var bbFrame1 = document.getElementById("bbFrame1"); bb_iframe_loaded = false; bbFrame1.src = bb_iframe_script + "?" + bb_count; } //When requested, save the current state //in a cache. function bb_save_state ( ) { //Store the new contents in the cache. var div_to_cache = document.getElementById(bb_target_div); bb_count++; bb_cache[bb_count] = div_to_cache.innerHTML; bb_debug_update ("Added " + bb_count + " to cache"); //Load the new page into the IFRAME. bb_loadframe ( ); bb_curr_idx = bb_count; } //Load the hidden IFRAME and start an interval timer. function bb_init (div_name, debug_val) { bb_target_div = div_name; bb_debug = debug_val; bb_loadframe ( ); window.setInterval ('bb_check_state( )', 1000); bb_save_state ( ); } Hacking the HackUsing a server-side script to update the contents of the hidden iframe may seem kludgy. We can read and write values into the iframe with JavaScript and thereby avoid the need for the count.cgi script, but unfortunately, some versions of Firefox (through at least 1.0.7) set the domain of the iframe to null after the back button is pressed, and then refuse to let the parent page access the iframe contents. As one kludge often leads to another, you may have also noticed that the server script calls a function in its onLoad event handler: <BODY onload='parent.bb_done_loading( );'> While count.cgi is very simple, it does take some small amount of time to run. If the iframe hasn't yet updated when the next bb_check_state( ) timer is called, the function may become confused. You can avoid this by having the iframe let its parent know explicitly when loading is completed. Hidden iframes are not the only approach to fixing the back button. As noted earlier, changing window.location works for some browsers, and for those browsers it's a simpler solution. You might even find a way to make it work with Internet Explorer as well. Mark Pruett |