jump to navigation

How to fix the Back and Forward buttons in AJAX February 22, 2006

Before we start explaining our fix, check out the finished product here: Popist Browse Page.

The popist browse page retrieves all the results by AJAX calls. When we first did the page, it didn’t have the back button functionality working, so if you did another search on the page and pressed the back button, you would end up on the page you were at before you reached the browse page, instead of the previous search. It was a ticket for a long time on our TRAC until we finally decided to fix the issue.

Now if you do a search and then click the next/previous links, or do another search, you will be able to press the back button and go back to your previous search. For that matter, if you press the forward button, you will go forward seamlessly as well.
We use the Rico library for all our AJAX needs, but it doesn’t matter what library or methods you use to make XmlHttpRequest calls. Our back/forward button fix is a wrapper around your existing methods.

This is obviously not the first solution to this problem. We found a great article by Mike Stenhouse on his Content with Style blog. Our method definitely drew from the ideas in his fix, as well as the techniques used in the articles he lists on his page.

Basic Concept

Every time an ajax call is made, the location of a hidden iframe is changed. These changes are recorded by your browser history so when you click on back or forward it reloads the iframe to the appropriate previous location. When the iframe reloads it calls a method in the parent frame and passes to it parameters from the url, telling the parent page what it needs to know to show the correct state.

Say you have a simple AJAX call which takes two numbers and returns the sum, like the demo example at Ajax Patterns.


<?
echo $_GET['number1'] + $_GET['number2'];
?>

Below is the code for the page which shows the basic AJAX operation. For the purposes of illustration, we use the code from AJAX Patterns again with small modifications to construct a very simple AJAX call.

AJAX client without back button fix


<html>
<head>
<script>

function createXMLHttpRequest() {
  try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) {}
  try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) {}
  try { return new XMLHttpRequest(); } catch(e) {}
  alert("XMLHttpRequest not supported");
  return null;
}

var xhReq = createXMLHttpRequest();

function callajax(number1, number2) {
  var servercall = "servermethod.php?number1=" +
  number1 + "&number2=" + number2;
  xhReq.open("GET", servercall, true);
  xhReq.onreadystatechange = ajaxresponse;
  xhReq.send(null);
}

function ajaxresponse() {
  if (xhReq.readyState != 4)
    return;
  var serverResponse = xhReq.responseText;
  document.getElementById("ajaxresponse").innerHTML = serverResponse;
}

</script>
</head>

<body>
<div id="ajaxresponse"></div>

number 1 <input type="text" id="number1" value="" 
                          name="number1" size="10" />
number 2 <input type="text" id="number2" value="" 
                          name="number2" size="10" />
<br>
<a href="javascript: callajax(document.getElementById('number1').value,
document.getElementById('number2').value);">
Call AJAX</a>
</body>
</html>
 

On our sample page, the Call Ajax link makes the call to the function callajax(number1, number2). It takes the numbers that are entered in the two input fields, passes them to the function, which then makes the request to servermethod.php. When the server response is returned, it is then put into the ajaxresponse div.

The back or forward buttons will not work on this page. If you enter in different numbers and get the sum, the page will not go back to the previous numbers you had. It will go instead to the page you were on before you reached the sample page. Below we show the technique using a few wrapper functions and a hidden iframe which will make this AJAX request back/forward button compatible.

First, add these three wrapper functions to the script:


function createfunctionstring() {
  var number1 = document.getElementById('number1').value;
  var number2 = document.getElementById('number2').value;
  var functionstring = "callajax(" + number1 + "," + number2 + ")";
  return functionstring;
}

function callajax_wrapper() {
// the function that you want called
  var functionstring = createfunctionstring();
window.frames["hiddeniframe"].location.href=
         "ajaxbackhelperiframe.html?functionstring="+escape(functionstring); 
}

function callfromiframe(functionstring) {
  eval(functionstring);
}

Change the body of the page to this:


<body>
<div id="ajaxresponse"></div>

number 1 <input type="text" id="number1" value="" 
                          name="number1" size="10" />
number 2 <input type="text" id="number2" value="" 
                          name="number2" size="10" />
<br>

<!-- change call from callajax to callajax_wrapper -->
<a href="javascript: callajax_wrapper();">Call AJAX</a>

<!-- add the back button helper iframe -->
<script language="javascript">
var functionstring = createfunctionstring();
document.write("<iframe name='hiddeniframe'
src=\"ajaxbackhelperiframe.html?functionstring=" + functionstring + "\"
style='visibility:hidden'></iframe>");
</script>
</body>

The above code is in this file, AJAX client with back button fix
The code in the hidden iframe, Iframe file for Back button, is as follows:


<html>
<head>
<script language="javascript">

function parseurl()
{
  var url=window.location.href;
  var functionstring=url.split("?")[1];

  if(!functionstring)
    return null;

  functionstring=functionstring.substr(("functionstring=").length);
  functionstring=unescape(functionstring);
  return functionstring;
}

function body_onload()
{
  var functionstring=parseurl();
  if(functionstring)
    parent.callfromiframe(functionstring);
}
</script>
</head>
<body onload="body_onload()">
</body>
</html>

How it Works:

First, in the body of the main page we added the code for the hidden iframe:


<script language="javascript">
var functionstring = createfunctionstring();
document.write("<iframe name='hiddeniframe'
src=\"ajaxbackhelperiframe.html?functionstring=" + functionstring + "\"
style='visibility:hidden'></iframe>");
</script>

This adds the iframe into the page and makes it hidden. When the page is loaded the script calls the function createfunctionstring, which takes the numbers in the input field, and constructs the string for the function that we want to call. For instance, if the number in the first field is 4, and the number in the second field is 23, then createfunctionstring will return the string “callajax(4, 23)”. This string is then pasted in the query string of the call to ajaxbackhelperiframe.html. In this manner, the function that represents the call to make is passed to the hidden iframe.

When the iframe is loaded, the body onload handler function, body_onload is called.


function body_onload()
{
  var functionstring=parseurl();
  if(functionstring)
    parent.callfromiframe(functionstring);
}

It parses the querystring to find the function string. Then, if it finds a functionstring, it makes a call to the callfromiframe function,


parent.callfromiframe(functionstring);

which is actually one of the functions we added to the main page:


function callfromiframe(functionstring) {
  eval(functionstring);
}

This function simply evaluates the functionstring which is passed to it. In our case, if the functionstring is “callajax(4, 23)”, the ajax request will be made with those two numbers.

Now, what happens if we enter in two different numbers in the fields and call ajax again? The code for the link that calls ajax is this:


<a href="javascript: callajax_wrapper();">Call AJAX</a>

So when we click on the link, the function callajax_wrapper is called, instead of the original callajax function. callajax_wrapper() is:


function callajax_wrapper() {
// the function that you want called
  var functionstring = createfunctionstring();
  window.frames["hiddeniframe"].location.href=
          "ajaxbackhelperiframe.html?functionstring="+escape(functionstring);
}

The first thing callajax_wrapper does is create the function string again,


var functionstring = createfunctionstring();

createfunctionstring() checks the numbers in the fields and constructs the new function call. Then the hidden iframe is made to reload with this new function call in the query string, through the following location.href:


window.frames["hiddeniframe"].location.href=
         "ajaxbackhelperiframe.html?functionstring="+escape(functionstring);

When the iframe reloads, the body_onload() function will be called again, and then callfromiframe() will evaluate the new function string and make the ajax call as before.

The trick here is that we always let the hidden iframe make the AJAX request. The net result is that once callajax_wrapper is called, the iframe reload triggers the body onload event which then makes the ajax call with the correct arguments. The hidden iframe keeps track of the history since the query string keeps changing with the functions that are called on the page. As it turns out, if the URL of the hidden iframe has changed, then that constitutes a change for the parent as well. When you press the back or forward buttons, the URL of the hidden iframe goes back and forward in the history. It stays on the same URL of the parent page, but makes the previous AJAX request, preserving back button functionality. Check out your back and forward buttons now, they will be working!

Subtleties:

When we change the body for the back button fix, we add this script at the bottom of the body of the page:


<script language="javascript">
var functionstring = createfunctionstring();
document.write("<iframe name='hiddeniframe'
src=\"ajaxbackhelperiframe.html?functionstring=" + functionstring + "\"
style='visibility:hidden'></iframe>");
</script>

There are a couple of subtleties with the way we did this. First, if the iframe is just written into the body somewhere without the document.write, then when the iframe would load up, it would call body_onload, but without the correct function string. Putting it at the end of the body with a document.write ensures that the rest of the body has already loaded by the time the body onload of the iframe occurs.

Comments

Sorry comments are closed for this entry