Wednesday, March 10, 2010

YUI Ajax Browser History Back Button Implementation

I've been looking for some better solutions to the whole Ajax browser back button issue and I think I've found the ideal candidate - YUI. For those who don't know much of this problem, I'll do a little recap. If you already understand the problem, feel free to jump right to the YUI implementation.

Detailed theoretical explanation of the problem and solutions - basically, browser's back (and forward) button is meant to take the user to either the last page the user was on or the last location in the same page the user is currently at. Ajax updates though, just "change" a part of the page - in other words, they don't take the user to a new "location". Consequently, since the location is still the same (only a part of the page changed, but addressbar still has the same location) browser doesn't register it as a "change" and the back button is never updated. So effectively, browser's back button doesn't work for the Ajax updates. This is a big problem since more and more websites are using Ajax updates for streamlined user interaction and without a functional back button, things like navigation menus and wizards become unusable.

There is a nifty solution to this problem though - Since the browsers rely on the changes in location to make the back button functional, we can change the URL fragments and fool the browser into thinking that the page has changed. URL fragments are stuff that comes after "#" in the location and generally they represent internal links. So, by changing the URL fragments with each Ajax update, we can make the back button functional. But since the browsers see the changes in the URL fragments as internal links and not as Ajax updates, they still don't know exactly what changed! As far as the browsers are concerned, changes in URL fragments mean that the user just moved to a different part of the page but the contents of the page are exactly the same. Since they don't track what changed, they can't go back to what was before - meaning, clicking the back button doesn't take the user to the last state, it doesn't do anything. This means that we need to keep track of whenever the user clicks back and forward buttons. We can actually put the state information associated with the Ajax calls in the URL fragments and use it to update the page as back/forward buttons are clicked.

But there is another problem - browsers don't expose any events for back/forward button clicks! So we can't tell when the user is going back or going forward and all that we can do is to poll and assume that the user clicked back/forward based on the current URL fragment. Further, we can use an iframe to store any data because an iframe won't be affected by navigation. This is basically the approach that is used by the popular browser history management solutions. Below is my experience with some of them -

1) ASP.NET Ajax History - This is very easy to use and just requires setting the EnableHistory attribute of ScriptManager and implementing the Navigate event. The problem with this though is that ASP.NET posts the whole page back anyway (whole page is posted and received) - so if the viewstate is even moderately big, it can be very slow. This pretty much defeats part of the purpose of doing Ajax updates where we want to post only the essential data and receive only the updated parts of the page, to make it as quick as we can. This is also the problem with ASP.NET's whole Ajax management system.

2) Really Simple History (RSH) - I've used with ASP.NET Ajax history and as a sole history management solution....and it works.....more or less. The problem is that its somehow hard to get right at the very first attempt. As you'd see in the comments here, pretty much every one had problems trying to set it up. But that's not really a 'problem', per say. The problem is that it hasn't worked for me in some versions of Internet Explorer (IE) and as we all know, IE is the most popular browser out there. It could just be my implementation but the way I look at it - it's just a means to an end, if something else exists that can solve the same problem with less effort then just use that other thing.....

Which brings us to....

3) YUI Browser History Manager - I very recently tried to implement it and I was suprised that it "just worked" in the very first try and without any problems....or may be I've just got smarter...whatever the case may be, I've successfully used it for many of my projects now and would recommend it. Below is some information on how to implement it -

Implementing YUI Browser History Manager:

Step 1: Download the following javascript files and include them:
<script type="text/javascript" src="/jquery.min.js"></script> (jquery needed for YUI)
<script type="text/javascript" src="/yahoo-dom-event.js"></script>
<script type="text/javascript" src="/connection.js"></script>
<script type="text/javascript" src="/history.js"></script> (from YUI - http://developer.yahoo.com/yui/history/)


Step 2: Add the below code right after the body tag
<iframe id="yui-history-iframe" src="assets/blank.html" width="1px" height="1px">
</iframe>
<input id="yui-history-field" type="hidden" />

(Note: iframe is needed only for IE)

Step 3: Add the below code right before the end of the body tag (or wherever you put your javascript)
<script type="text/javascript">
var bookmarked = YAHOO.util.History.getBookmarkedState("state");
var init = bookmarked || query || "page1.aspx";
YAHOO.util.History.register("state", init, OnStateChanged);
try {
YAHOO.util.History.initialize("yui-history-field", "yui-history-iframe");
} catch (e) {
}
YAHOO.util.History.onReady(function () {});
</script>


Now, if all you care about are back/forward buttons for one set of Ajax calls, then you don't need to change anything in the above code. But if you also want page refreshes and bookmarking support, then some more code will go into the onReady method above (I'll be writing another post for bookmarking support).

Step 4: Add the below code to all your Ajax calls.

YAHOO.util.History.navigate("state", param);

Note the "param" argument above - this is where you store the state information that you'll be using when back/forward buttons are clicked. Below is a sample code:

//Ajax call that needs browser's back button to work
function AjaxShowAppleImage()
{
// Ajax code that shows an apple's image
$('#img1').attr('src', 'apple.jpg');
//YUI code
YAHOO.util.History.navigate("state", 'apple');
}
//Another ajax call that needs browser's back button to work
function AjaxShowOrangeImage()
{
// Ajax code that shows an apple's image
$('#img1').attr('src', 'orange.jpg');
//YUI code
YAHOO.util.History.navigate("state", 'orange');
}

Step 5: Implement a OnStateChanged function. This OnStateChanged is called whenever the back/forward buttons are clicked and you use the "state" parameter (from above) to update the page (in our example - show the correct fruit image). Below is a sample explanation -

function OnStateChanged(state)
{
if(state == 'apple')
$('#img1').attr('src', 'apple.jpg');
else if(state == 'orange')
$('#img1').attr('src', 'orange.jpg');
}

And that's all you need to do. Pretty simple, isn't it?

This ends this post. For those who would like to grab some working code as a starting point, you can take the below code. It has been tested on most popular browsers and works perfectly. It basically contains a couple of links that load the content from two pages (page1.aspx and page2.aspx). It assumes that both these pages have a div with the id as "content" and loads their content in a div with an id "loadedContent". Have fun ajaxing! :)

(UPDATE 06-07-2010: Fixed a minor bug related to missing comma in the YAHOO.util.History.register method after "init" in the below code - YAHOO.util.History.register("state", init, function(state)... - Thanks to Mike in the comments for pointing it out)


<iframe id="yui-history-iframe" src="assets/blank.html" width="1px" height="1px">
</iframe>
<input id="yui-history-field" type="hidden" />
<script type="text/javascript" src="/jquery.min.js"></script>
<script type="text/javascript" src="/yahoo-dom-event.js"></script>
<script type="text/javascript" src="/connection.js"></script>
<script type="text/javascript" src="/history.js"></script>
<form id="form1" runat="server">
<div>
<a href="page1.aspx" onclick="$('#loadedContent').load('page1.aspx #content');YAHOO.util.History.navigate('state', 'page1.aspx');return false;">
page1</a> <a href="page2.aspx" onclick="$('#loadedContent').load('page2.aspx #content');YAHOO.util.History.navigate('state','page2.aspx');return false;">
page2</a>
</div>
<div id="loadedContent">
</div>
</form>

<script type="text/javascript">
var bookmarked = YAHOO.util.History.getBookmarkedState("state");
var query = YAHOO.util.History.getQueryStringParameter("divs");
var init = bookmarked || query || "page1.aspx";
YAHOO.util.History.register("state", init, function(state) {$('#loadedContent').load(state + ' #content');});
try {
YAHOO.util.History.initialize("yui-history-field", "yui-history-iframe");
} catch (e) {
}
YAHOO.util.History.onReady(function () {});
</script>

5 comments:

  1. it does not work ; please test your tutorial before releasing it to the masses !

    thanks for the effort thow

    ReplyDelete
  2. It works for me. It was posted after testing on all the major browsers and the only reason its not working for you would be that you either missed something in the implementation or there is some minor lexical problem. If you can give us more details on the errors that you are getting, I can probably help you out. Or if you are sure that there is some bug in the code and can point it out, please do as this will help others as well!

    ReplyDelete
  3. I also found that the code in your example doesn't work.The Console advises me that there is a missing ')' after the argument list, and I am assuming that this refers to the missing (?) argument for YAHOO.util.History.register. Your example code appears to list only two arguments, when, I think, three are required. A comma needs to be added between 'init' and function (state)... ,as follows:YAHOO.util.History.register("state", init, function(state) {$('#loadedContent').load(state + ' #content');});

    ReplyDelete
  4. Hello Mike, thanks for pointing out the missing comma. It seems to be a typo but otherwise the code works fine. I've fixed it now.

    Manoj

    ReplyDelete
  5. Could someone please help me implementing this on my website? The link is down below.

    ReplyDelete