100% found this document useful (3 votes)
105 views17 pages

AJAX: How To Handle Bookmarks and Back Buttons, Advanced Example

This blog post discusses an advanced example of using the Really Simple History framework to handle bookmarks and browser back/forward buttons in AJAX applications. The example application loads topic contents dynamically and records history using the DhtmlHistory and HistoryStorage APIs. When the page loads, it checks if it is the first load or a reload, loads topic data from an XML file if needed, initializes the UI, and sets up handlers for user clicks and browser history events.

Uploaded by

raha.argha3027
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOC or read online on Scribd
100% found this document useful (3 votes)
105 views17 pages

AJAX: How To Handle Bookmarks and Back Buttons, Advanced Example

This blog post discusses an advanced example of using the Really Simple History framework to handle bookmarks and browser back/forward buttons in AJAX applications. The example application loads topic contents dynamically and records history using the DhtmlHistory and HistoryStorage APIs. When the page loads, it checks if it is the first load or a reload, loads topic data from an XML file if needed, initializes the UI, and sets up handlers for user clicks and browser history events.

Uploaded by

raha.argha3027
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOC or read online on Scribd
You are on page 1/ 17

AJAX: How to Handle Bookmarks and Back

Buttons, Advanced Example


This blog post walks users through advanced usage of the Really
Simple History framework, providing ancillary information for the soon
to be published O'Reilly Network article "AJAX: How to Handle
Bookmarks and Back Buttons." The Really Simple History framework
consists of two open source JavaScript classes, DhtmlHistory and
HistoryStorage. These two classes make it possible for AJAX applications
to support bookmarking and the back and forward buttons.

To begin, let's see what our application will look like when finished. We
will create an AJAX web page with a series of topic links; when topics
are selected, their contents will be remotely loaded and shown on the
right-hand side of the page without performing a full page refresh:

Figure 1. Advanced Example Screenshot (Click for Screen cast of User


Interacting With Example)

The example is somewhat arbitrary and could be created with other,


simpler technologies, such as using an iframe and hyperlinks that
target the iframe. This example, however, is straightforward enough to
illustrate how to use the Really Simple History API in an advanced way
for controlling history, providing bookmarking, and caching state that is
expensive to reload from the server. It is meant to provide cut and
paste code for more advanced AJAX applications.

The example application consists of four major files: advanced.html,


advanced.css, advanced.js, and topics.xml. We also use three frameworks
to accelerate development: the DhtmlHistory and HistoryStorage APIs;
the X DHTML Library, an open source toolkit that eases cross-browser
DHTML; and Sarissa, an open source API for working with XmlHttpRequest
and XML.

As users select topics in the left hand menu of our sample application,
we fetch the topic's contents in the background and use the
DhtmlHistory API to record bookmarkable history. When the page is
initially loaded, we retrieve the list of available topics from an XML file
and cache them locally using the HistoryStorage API, using this XML file
to build up our user interface dynamically.
Let's begin with the XML file that holds our list of available sidebar
topics, named topics.xml; we will use this file to create our initial user
interface:

<?xml version="1.0" encoding="ISO-8859-1"?>

<topics>
<topic id="topic1"
title="Topic 1"
src="topic1.html"
default="true"/>

<topic id="topic2"
title="Topic 2"
src="topic2.html"/>

<topic id="topic3"
title="Topic 3"
src="topic3.html"/>
</topics>

This is a simple XML file that records each available topic. We assign
each topic several attributes, such as id and title, that we will later
extract using JavaScript to build up the left-hand menu sidebar. Using
an XML file in our application is meant to simulate more sophisticated
AJAX pages that need to cache complex resources.

Next, we create the scaffolding for advanced.html. We declare our HTML


doctype and link in our JavaScript libraries:

<!DOCTYPE HTML PUBLIC


"-//W3C//DTD HTML 4.0 Strict//EN">

<html>
<head>
<!-- The X Cross-Browser DHTML Library -->
<script language="JavaScript"
src="../lib/x/x_core.js">
</script>
<script language="JavaScript"
src="../lib/x/x_dom.js">
</script>
<script language="JavaScript"
src="../lib/x/x_event.js">
</script>

<!-- Load the DhtmlHistory and


HistoryStorage APIs -->
<script type="text/javascript"
src="../lib/history/serializer.js">
</script>
<script type="text/javascript"
src="../lib/history/historyStorage.js">
</script>
<script type="text/javascript"
src="../lib/history/dhtmlHistory.js">
</script>

<!-- The Sarissa Library -->


<script type="text/javascript"
src="../lib/sarissa/sarissa.js">
</script>

<!-- Our application's JavaScript -->


<script type="text/javascript"
src="advanced.js"></script>

We create our basic HTML markup and style it using an open source,
two-column layout, Cascading Style Sheets (CSS) template. We store
the CSS separate from the HTML in advanced.css (view source) to ease
maintenence. The complete advanced.html:

<html>
<head>
<!-- The X Cross-Browser DHTML Library -->
<script language="JavaScript"
src="../lib/x/x_core.js">
</script>
<script language="JavaScript"
src="../lib/x/x_dom.js">
</script>
<script language="JavaScript"
src="../lib/x/x_event.js">
</script>

<!-- Load the DhtmlHistory and


HistoryStorage APIs -->
<script type="text/javascript"
src="../lib/history/serializer.js">
</script>
<script type="text/javascript"
src="../lib/history/historyStorage.js">
</script>
<script type="text/javascript"
src="../lib/history/dhtmlHistory.js">
</script>

<!-- The Sarissa Library -->


<script type="text/javascript"
src="../lib/sarissa/sarissa.js">
</script>

<!-- Our application's JavaScript -->


<script type="text/javascript"
src="advanced.js"></script>

<!-- Link in style sheets -->


<link rel="stylesheet"
type="text/css"
href="advanced.css">
</link>
</head>

<body>
<div id="content">
<h1 id="topicTitle">
Topic Title
</h1>

<div id="topicContent">
Topic Content Goes Here
</div>
</div>

<div id="menu">
</div>
</body>
</html>

Let's move on to advanced.js. First, we add an onload listener to the


window, calling a method named initialize() when the page has
finished loading:

// initialize ourselves when the page is finished


// loading
window.onload = initialize;

Much of the bulk of our application is in initialize(). Let's see the full
initialize() method, which we will break down and go over piece by
piece:

// an array of our topics


var topics = new Array();

function initialize() {
// initialize the DhtmlHistory
// framework
dhtmlHistory.initialize();

// if this is the first time the page


// has loaded, fetch the list of
// topics remotely
if (dhtmlHistory.isFirstLoad()) {
topics = loadTopics();
historyStorage.put("topics", topics);
}
else {
// else, simply extract it from our
// history storage
topics = historyStorage.get("topics");
}
// display our topics list
displayTopicsList();

// initialize our initial state from


// the browser location after the hash
var currentTopic =
dhtmlHistory.getCurrentLocation();
displayTopic(currentTopic);

// catch when a user clicks on a new


// topic
var menu =
document.getElementById("menu");
xAddEventListener(menu, "click",
handleTopicChange,
false);

// set ourselves up to listen to


// history events
dhtmlHistory.addListener(
handleHistoryEvent);
}

In initialize(), we first create and instantiate the DhtmlHistory library


by calling dhtmlHistory.initialize():

// an array of our topics


var topics = new Array();

function initialize() {
// initialize the DhtmlHistory
// framework
dhtmlHistory.initialize();

// if this is the first time the page


// has loaded, fetch the list of
// topics remotely
if (dhtmlHistory.isFirstLoad()) {
topics = loadTopics();
historyStorage.put("topics", topics);
}
else {
// else, simply extract it from our
// history storage
topics = historyStorage.get("topics");
}

// display our topics list


displayTopicsList();

// initialize our initial state from


// the browser location after the hash
var currentTopic =
dhtmlHistory.getCurrentLocation();
displayTopic(currentTopic);
// catch when a user clicks on a new
// topic
var menu =
document.getElementById("menu");
xAddEventListener(menu, "click",
handleTopicChange,
false);

// set ourselves up to listen to


// history events
dhtmlHistory.addListener(
handleHistoryEvent);
}

In initialize(), we must differentiate between the first time this page


has loaded versus the user navigating back to it in their history;
initialize() is called in both instances. We use the
dhtmlHistory.isFirstLoad() method to check for this.

If this is the first time the page has loaded, we call a method named
loadTopics() to retrieve the list of available topics. loadTopics() uses the
XmlHttpRequest object to remotely fetch the topics.xml file, parse out its
values, and then uses them to create a JavaScript array containing
each topic value. loadTopics() returns an array of available topics,
which we persist into the History Storage API under the hashtable key
"topics". If the user navigates away from this page and then back, the
value will still be available inside the historyStorage class:

// an array of our topics


var topics = new Array();

function initialize() {
// initialize the DhtmlHistory
// framework
dhtmlHistory.initialize();

// if this is the first time the page


// has loaded, fetch the list of
// topics remotely
if (dhtmlHistory.isFirstLoad()) {
topics = loadTopics();
historyStorage.put("topics", topics);
}
else {
// else, simply extract it from our
// history storage
topics = historyStorage.get("topics");
}

// display our topics list


displayTopicsList();

// initialize our initial state from


// the browser location after the hash
var currentTopic =
dhtmlHistory.getCurrentLocation();
displayTopic(currentTopic);

// catch when a user clicks on a new


// topic
var menu =
document.getElementById("menu");
xAddEventListener(menu, "click",
handleTopicChange,
false);

// set ourselves up to listen to


// history events
dhtmlHistory.addListener(
handleHistoryEvent);
}

Let's break out the loadTopics() method. This method first fetches the
remote topics.xml file using the Sarissa utility framework; Sarissa will
automatically handle cross-browser differences in terms of fetching the
file and rendering it into an XML DOM object that we can work with
easily. Next, we parse out the values in our XML file into a JavaScript
array that represents all of the available topics. The full loadTopics()
method:

function loadTopics() {
// load our remote topics.xml document
// synchronously into an XML DOM object
var topicsXML =
Sarissa.getDomDocument();
topicsXML.async = false;
topicsXML.load("topics.xml");

// parse out our topics from the XML,


// building up a JavaScript array that
// mirrors these values
var topics = new Array();
var topicElements =
topicsXML.getElementsByTagName(
"topic");
for (var i = 0;
i < topicElements.length;
i++) {
var currentTopic = new Object();
currentTopic.id =
topicElements[i].getAttribute(
"id");
currentTopic.title =
topicElements[i].getAttribute(
"title");
currentTopic.src =
topicElements[i].getAttribute(
"src");
currentTopic.isDefault =
topicElements[i].getAttribute(
"default");

if (currentTopic.isDefault == null ||
currentTopic.isDefault
== undefined)
currentTopic.isDefault = false;

// add a toString() method for


// debugging
currentTopic.toString = function() {
return "[id="+this.id
+ ", title="+this.title
+ ", src="+this.src
+ ", isDefault="
+ this.isDefault
+ "]";
};

topics.push(currentTopic);
}

return topics;
}

Once we have retrieved the available topics, either remotely on the


first page load or by retrieval from historyStorage on subsequent
navigations back to this page, we can display our list of available
topics. We use the displayTopicsList() method to do so; this method
uses the topics array to simply update the DOM with available topics:

function initialize() {
// initialize the DhtmlHistory
// framework
dhtmlHistory.initialize();

// if this is the first time the page


// has loaded, fetch the list of
// topics remotely
if (dhtmlHistory.isFirstLoad()) {
topics = loadTopics();
historyStorage.put("topics", topics);
}
else {
// else, simply extract it from our
// history storage
topics = historyStorage.get("topics");
}

// display our topics list


displayTopicsList();

// initialize our initial state from


// the browser location after the hash
var currentTopic =
dhtmlHistory.getCurrentLocation();
displayTopic(currentTopic);

// catch when a user clicks on a new


// topic
var menu =
document.getElementById("menu");
xAddEventListener(menu, "click",
handleTopicChange,
false);

// set ourselves up to listen to


// history events
dhtmlHistory.addListener(
handleHistoryEvent);
}

The displayTopicsList() method:

function displayTopicsList() {
var menu =
document.getElementById("menu");
for (var i = 0; i < topics.length;
i++) {
// use each topic to update
// our user interface
var newTopic =
document.createElement("a");

newTopic.href = topics[i].src;
newTopic.title = topics[i].title;
// note: avoid using the id attribute of
// hyperlinks if they will clash with a
// location you store into history; if these
// values are the same you can run into
// some strange bugs in Internet Explorer;
// to avoid this, we use setAttribute with
// a custom attribute named "topicID"
newTopic.setAttribute("topicID", topics[i].id);
newTopic.innerHTML = topics[i].title;

menu.appendChild(newTopic);
}
}

Our application must support being loaded from bookmarks. To do so,


we must determine our initial location, parse out values after the
anchor hash, and use these to initialize our application. For example, if
the user bookmarks our example page as
http://codinginparadise.org/example.html#topic3, then we will display the
third topic when this bookmark is selected.
In initialize(), when the first page loads, we must use the
dhtmlHistory.getCurrentLocation() method to get the current location
after the hash value; if there is none then an empty string is returned.
We pass the initial value to the displayTopic() method to show an initial
application state on page load:

function initialize() {
// initialize the DhtmlHistory
// framework
dhtmlHistory.initialize();

// if this is the first time the page


// has loaded, fetch the list of
// topics remotely
if (dhtmlHistory.isFirstLoad()) {
topics = loadTopics();
historyStorage.put("topics", topics);
}
else {
// else, simply extract it from our
// history storage
topics = historyStorage.get("topics");
}

// display our topics list


displayTopicsList();

// initialize our initial state from


// the browser location after the hash
var currentTopic =
dhtmlHistory.getCurrentLocation();
displayTopic(currentTopic);

// catch when a user clicks on a new


// topic
var menu =
document.getElementById("menu");
xAddEventListener(menu, "click",
handleTopicChange,
false);

// set ourselves up to listen to


// history events
dhtmlHistory.addListener(
handleHistoryEvent);
}

The displayTopic() method takes a topic ID as input, such as "topic1",


and either loads it from the server remotely or retrieves it from our
historyStorage cache if we have loaded it already. If the user does not
specify a topic on page load, then we simply display the default topic,
which we specified in our topics.xml file with a default="true" attribute.
displayTopic() uses the historyStorage.hasKey() method to determine if
we have already loaded the HTML content for this topic; if we have, we
can simply retrieve it locally without having to do a full server refresh.
If not, we use the Sarissa framework to retrieve the page remotely.
Once the page contents are loaded, we can swap their values into the
user interface. The full displayTopic() method:

function displayTopic(topicID) {
var topic;

// if no topic passed in then get the


// default topic
if (topicID == null ||
topicID == "") {
for (var i = 0; i < topics.length;
i++) {
if (topics[i].isDefault) {
topic = topics[i];
break;
}
}
}
else {
// fetch the topic with this ID
for (var i = 0; i < topics.length;
i++) {
if (topics[i].id == topicID) {
topic = topics[i];
break;
}
}
}

// see if we have cached the contents


// of this topic in our history storage
var content;
if (historyStorage.hasKey(topic.id)) {
content = historyStorage.get(
topic.id);
}
else {
// get the filename to load
var url = topic.src;

// load this file synchronously


var request = new XMLHttpRequest();
request.open("GET", url, false);
request.send(null);
content = request.responseText;

// persist this value into our


// history storage
historyStorage.put(topic.id, content);
}

// now place this content and title


// into the HTML
var topicTitle =
document.getElementById("topicTitle");
topicTitle.innerHTML = topic.title;
var topicContent =
document.getElementById(
"topicContent");
topicContent.innerHTML = content;

return content;
}

In initialize(), we must also register ourselves for onclick events so


that we can respond when a user selects a new topic. We do this using
xAddEventListener(), a utility method from the X DHTML library for
cross-browser event subscription. xAddEventListener takes four
arguments:

xAddEventListener(element, eventType, handler, useW3cBubbling)

elementis the element we are subscribing to; eventType is a string


containing the type of event to listen to, such as "click" or "load"
events; handler is a reference to a function that will receive the event
when it occurs; and isW3cBubbling is an advanced option that controls
whether to use W3C style bubbling. W3C style bubbling is not well
supported cross browser, so this value should always be false.

To receive new topic changes, we use the following code:

function initialize() {
// initialize the DhtmlHistory
// framework
dhtmlHistory.initialize();

// if this is the first time the page


// has loaded, fetch the list of
// topics remotely
if (dhtmlHistory.isFirstLoad()) {
topics = loadTopics();
historyStorage.put("topics", topics);
}
else {
// else, simply extract it from our
// history storage
topics = historyStorage.get("topics");
}

// display our topics list


displayTopicsList();

// initialize our initial state from


// the browser location after the hash
var currentTopic =
dhtmlHistory.getCurrentLocation();
displayTopic(currentTopic);

// catch when a user clicks on a new


// topic
var menu =
document.getElementById("menu");
xAddEventListener(menu, "click",
handleTopicChange,
false);

// set ourselves up to listen to


// history events
dhtmlHistory.addListener(
handleHistoryEvent);
}

We wire mouse clicks on the topics to a handler method named


handleTopicChange(). handleTopicChange()
is called when a user clicks on a topic hyperlink in the
sidebar:

function handleTopicChange(e) {
var evt = new xEvent(e);
var target = evt.target;
var topicID = target.getAttribute("topicID");

// display this topic


var content = displayTopic(topicID);

// add this to our history


dhtmlHistory.add(topicID, content);

// cancel the default behavior of hyperlinks


return evt.cancel();
}

This method first creates a cross-browser event object using the


X DHTML library's xEvent utility object.
xEvent takes a browser specific event object and
normalizes working with events, exposing standard properties such
as 'target' to get the target:

function handleTopicChange(e) {
var evt = new xEvent(e);
var target = evt.target;
var topicID = target.getAttribute("topicID");

// display this topic


var content = displayTopic(topicID);

// add this to our history


dhtmlHistory.add(topicID, content);
// cancel the default behavior of hyperlinks
return evt.cancel();
}

Next, we extract the topic ID the user wants and display this
topic:

function handleTopicChange(e) {
var evt = new xEvent(e);
var target = evt.target;
var topicID = target.getAttribute("topicID");

// display this topic


var content = displayTopic(topicID);

// add this to our history


dhtmlHistory.add(topicID, content);

// cancel the default behavior of hyperlinks


return evt.cancel();
}

We take this topic's content and ID and add it to our DHTML history,
which will automatically update the browser location and persist this
history data:

function handleTopicChange(e) {
var evt = new xEvent(e);
var target = evt.target;
var topicID = target.getAttribute("topicID");

// display this topic


var content = displayTopic(topicID);

// add this to our history


dhtmlHistory.add(topicID, content);

// cancel the default behavior of hyperlinks


return evt.cancel();
}

Finally, the handleTopicChange() method cancels the


default hyperlink action, which is to navigate to a new page; it
uses the evt.cancel() method to do so, which must be
returned from the method:

function handleTopicChange(e) {
var evt = new xEvent(e);
var target = evt.target;
var topicID = target.getAttribute("topicID");

// display this topic


var content = displayTopic(topicID);
// add this to our history
dhtmlHistory.add(topicID, content);

// cancel the default behavior of hyperlinks


return evt.cancel();
}

We are almost finished fleshing out our page's initialization routine in


initialize(). Our final initialization step is to register ourselves to
receive history events with the DhtmlHistory framework:

function initialize() {
// initialize the DhtmlHistory
// framework
dhtmlHistory.initialize();

// if this is the first time the page


// has loaded, fetch the list of
// topics remotely
if (dhtmlHistory.isFirstLoad()) {
topics = loadTopics();
historyStorage.put("topics", topics);
}
else {
// else, simply extract it from our
// history storage
topics = historyStorage.get("topics");
}

// display our topics list


displayTopicsList();

// initialize our initial state from


// the browser location after the hash
var currentTopic =
dhtmlHistory.getCurrentLocation();
displayTopic(currentTopic);

// catch when a user clicks on a new


// topic
var menu =
document.getElementById("menu");
xAddEventListener(menu, "click",
handleTopicChange,
false);

// set ourselves up to listen to


// history events
dhtmlHistory.addListener(
handleHistoryEvent);
}

handleHistoryEvent() is a JavaScript method that will be called whenever


a history event is fired. History events can happen from a variety of
user actions, such as navigating with the back and forward buttons,
selecting an AJAX bookmark in their browser, manually modifying the
browser location, etc. AJAX pages will only receive history events that
pertain to them, due to the browser's security model. If the user
navigates away from your AJAX web page, for example, no history
event is thrown; if the user navigates back, however, a history event
will be returned to your application.

handleHistoryEvent() is straightforward; it simply takes the newLocation


and uses this to display the selected topic:

function handleHistoryEvent(newLocation,
historyData) {
var topicID = newLocation;

// display this topic


displayTopic(topicID);
}

Our advanced application is now finished! Demo the advanced


application or see the source code: advanced directory, advanced.html,
advanced.css, advanced.js, topics.xml.

// posted by Brad Neuberg @ Thursday, September 15, 2005


Comments:
This post has been removed by a blog administrator.
# posted by ron mac : 11:34 AM

Hi Brad,

Great article! I got it working great in Firefox, and then moved over to
IE6 and am not having any luck. When I add a new location to the
history, I don't seem to get any trigger from the history listener to go
the new location.

There is one main difference between your implementation and mine -


I do not use the X library. Would that make a difference? From my href I
call a javascript function - do I need to return anything from it to make
this work? The add function does not throw an exception either -
checked that already.

I will keep looking into it, but any additional guidance would be greatly
appreciated.

Thanks,
Ron

You might also like