The HTML5 History API: Making History with AJAX - Part II

Amarjit Bharath   —   2 April 2013   —   Code & Drupal

In the last article, we covered the use of AJAX and how problems can arise as a result from not handling the browser history and URLs within the address bar correctly.
In this part, we will cover how to implement a simple AJAX site and manipulate the browser history using the pushState() method of the history object.

If you missed the first one, head over to The HTML5 History API: AJAX & URLs (or no URLs) – Part I as a prerequisite to understand the problems we are solving with these examples. If you do want to just learn about AJAX and the History API by example, this is also accustomed to yourself.

We will start by setting up a basic HTML site, adding the AJAX functionality and finally adding some code to manipulate the browser history and address bar. The code will be kept as simple as possible; thus no external libraries like JQuery will be used, or even HTML5 elements, as this would require using an HTML5shiv.

Let’s get started in making history.

The HTML Structure

We’ll start off by first setting up the basic HTML structure. Below is the HTML5 structure, so nothing special here.

home.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>HTML5 History API</title>
</head>
<body>
 
  <div id="container">
    <a id="home-link" href="home.html" target="_blank">Home</a> | 
    <a id="contact-link" href="contact.html" target="_blank">Contact</a> | 
    <a id="about-link" href="about.html" target="_blank">About</a>
   
    <h1>Home</h1>
    <p style="padding: 20px; background: green; color: yellow;">Bacon ipsum dolor sit amet chicken cow strip steak sausage drumstick meatball.</p>
  </div>
    
  <script src="script.js"></script>
  
</body>
</html>

You will notice I have included a script.js reference just before the closing body tag. Go ahead and create this as an empty file. Later, we will use this file to hold our application.

The 3 links represent a navigation menu and corresponds to the pages that we will be loading using AJAX. If you save the first code block as home.html, then duplicating this file twice; naming them about.html and contact.html. Change the heading and content as needed in both files. I have included the extra 2 pages below with a little inline styling for ease sake.

about.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>HTML5 History API</title>
</head>
<body>
 
  <div id="container">
    <a id="home-link" href="home.html" target="_blank">Home</a> | 
    <a id="contact-link" href="contact.html" target="_blank">Contact</a> | 
    <a id="about-link" href="about.html" target="_blank">About</a>
   
    <h1>About</h1>
    <p style="padding: 20px; background: red; color: yellow;">Bacon ipsum dolor sit amet chicken cow strip steak sausage drumstick meatball.</p>
  </div>
    
  <script src="script.js"></script>
  
</body>
</html>

contact.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>HTML5 History API</title>
</head>
<body>
 
  <div id="container">
    <a id="home-link" href="home.html" target="_blank">Home</a> | 
    <a id="contact-link" href="contact.html" target="_blank">Contact</a> | 
    <a id="about-link" href="about.html" target="_blank">About</a>
   
    <h1>Contact</h1>
    <p style="padding: 20px; background: orange; color: yellow;">Bacon ipsum dolor sit amet chicken cow strip steak sausage drumstick meatball.</p>
  </div>
    
  <script src="script.js"></script>
  
</body>
</html>

AJAXIFY

The basic structure of the page is now place. If you load the site in your browser, you can happily navigate through the site as normal. You will notice that the links work like, well like regular links. The browser redirects every time you click the link and as a result; the address bar is updated. Again nothing special here, as the links contain well formed URLs.

The next step is to add the AJAX functionality, which allows content to be loaded in the browser dynamically; thus content is loaded without the page refreshing or the browser redirecting.
We will be adding all of our JavaScript code into the script.js file you created earlier.

First of all, let's setup a timer that constantly checks if the DOM has finished loading. When the DOM has finally finished loading, the timer will be destroyed and we will execute an initializing function setupLinks(). This snipped of code is the essentially the same as using JQuery's document.ready().

1
2
3
4
5
6
7
8
9
// Timer to check when DOM is ready.
var readyStateCheckInterval = self.setInterval(function() {
    // Check if DOM ready.
    if (document.readyState === "complete") {
        // When DOM ready, destroy timer and execute setupLinks() function.
        clearInterval(readyStateCheckInterval);
        setupLinks();
    }
}, 10);

Binding the Links

When the DOM is ready, setupLinks() is executed. This function will simply send the link elements off to be 'ajaxified'. Within this function, we will setup the predefined links; home, about and contact. You may be thinking it is simpler to call the ajaxify_link() directly from the timer, however our example is over simplified. You should substitute this with code that will scan for links in certain regions of your page and bind them to the click events by sending them to ajaxify_link(). For our example, we will simply provide a set of 3 element links.

1
2
3
4
5
6
7
8
9
10
/*
 * Function to bind links to click listener,
 * which will pull content from HREF value.
 */
function setupLinks() {
  // Send our predefined link elements to ajaxify_link() function.
  ajaxify_link(document.getElementById("home-link"));
  ajaxify_link(document.getElementById("contact-link"));
  ajaxify_link(document.getElementById("about-link"));
}

Now IE8 and downwards has inconsistencies with other browsers, as usual. To cater for both we must use the appropriate JavaScript function calls that are native to those browsers. The ajaxify_link() below binds a click event listener for the link elements we passed in. When a user clicks the link, the default browser action is suppressed (redirecting to the link) using preventDefault() and then executes the getPage() function. This function will replace the main container element in our page with a particular element from the page pulled in using AJAX.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
 * Function to add click event to elements.
 * Modifies browser URL and pulls content using XMLRequestObject.
 * Default click is disabled - thus browser is not redirected to href.
 */
function ajaxify_link(link_element) {
 
  // Destroy any currently bound click events to this element.
  link_element.onclick = null;
 
  // IE8
  if (!link_element.addEventListener) {
    link_element.attachEvent("onclick", function(e) {
      // Prevent default browser action on click.
      // This will stop the browser redirecting to  the HREF value.
      e.returnValue = false;
 
      // Get page and replace current content.
      getPage(link_element.href);
    }, false);
  }
  
  // FF, Chrome, Safari, IE9.
  else {
    link_element.addEventListener("click", function(e) {
      // Prevent default browser action on click.
      // This will stop the browser redirecting to  the HREF value.
      e.preventDefault();
 
      // Get page and replace current content.
      getPage(link_element.href);
    }, false);
  }
}

Fetching data - XMLRequestObject

We now have our structure in place and links that have been bound to click events. The getPage() function is called when a user clicks a link. This functions purpose is to extract the URL from the link and then fetch the data, which in our case would be the home, about or contact page. The data contains a particular element (#container), which we will extract and replace the current container element on the page. Thus the browser will not direct to another page or refresh.

You will notice that most examples around the web, simply replace the whole page with whatever the AJAX call returns. This is usually the whole document, including the <html> tag. We will not be following these examples. Instead, we will fetch the page and then extract what we need using a little DOM trick.

We will rebind all the links on the page again, to ensure that every link is accounted for. In a real world example, your setupLinks() function would be adjusted so it takes care of all your links automatically; you don't want to have the burden of maintaining a list of manually entered links.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
 * Function to fetch page via URL.
 */
function getPage(link_url) {
    // Get content using XMLHttpRequest object.
    XMLHttp = new XMLHttpRequest();
    
    // Set the request type, URL and disable asynchronous mode,
    // as we don't care about the script hanging waiting for the reply.
    XMLHttp.open("GET", link_url, false);
    
    // Send the request to the server.
    XMLHttp.send();
    
    // The response we receive is not in XML format. So we cannot 
    // use the DOM to extract what we need directly.
    // Thus we assign it to a new element first.
    var dom_response_holder = document.createElement('div');
    
    // Remember to use 'responseText' instead of 'response', as this won't work in IE.
    dom_response_holder.innerHTML = XMLHttp.responseText;
 
    // Now extract what we need using the DOM.
    var new_container_element = dom_response_holder.getElementsByTagName("DIV");
    
    // getElementsByTagName() returns an array. We are only interested in the first element.
    new_container_element = new_container_element[0].innerHTML;
        
    // Replace current content.
   document.getElementById("container").innerHTML = new_container_element;
 
    // Rebuild links, to ensure all links are bound correctly.
    setupLinks();
}

If you attempt to test this code by simply opening the page within your browser, the AJAX request will fail. This is due to the cross-domain policy of JavaScript. Instead, you will need to setup a local HTTP server locally or remotely. If you don’t have access to a server, you can simply use a free service such as jsfiddle.net.

After you have got this code being served from a HTTP server, you will notice that content is loaded dynamically but the URL is never updated, as expected. Try navigating through content using the navigation links and then bookmarking the page. As discussed in Part I; it will only ever bookmark the first page you landed on.

However, our AJAX site is setup so we only need to update the URL within the browser address bar and ensure our browser is logging our history.

History API: pushState()

The HTML5 History API allows us to update the address bar and the actual history within the browser using the pushState() function. If a browser does not support pushState(), we will revert back to using hashes. Hashes are indeed ugly but we should allow all users to be able to share, bookmark and use functional URLs.

The pushState() function accepts 3 parameters:

  1. Data - Allows you to store any data that can be serialized. Also known as state data. Storage is limited, Firefox allows 640KB for example. You could store active menus or page titles or whatever you need recording between pages. You can also access this data if the user refreshes the page.
  2. Page Title - The page title that appears in the tab within the browser. This is not implemented very well across all browsers. Firefox simply ignores this parameter.
  3. URL - The URL that will be placed in the address bar. This an either be relative or absolute path; the browser will work it out itself.

You can find out more about the History API at WHATWG.

Knowing the prototype of the function, we will create a function that does all the above. We will implement a fallback for browsers that do not support the pushState() function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
 * Function to modify URL within browser address bar.
 */
function changeBrowserURL(dom_element) {
  // Change URL with browser address bar using the HTML5 History API.
  if (history.pushState) {
    // Parameters: data, page title, URL
    history.pushState(null, null, dom_element.href);
  }
  // Fallback for non-supported browsers.
  else {
    document.location.hash = dom_element.getAttribute("href");
  }
}

The remaining task is to have the newly created changeBrowserURL() function to be called within the script. For our example, we will execute this function as soon as the user clicks a link. This should however really be placed after getPage() returns a successful message that the AJAX content has been loaded.
To keep things simple, we'll place it with the click event.

See the amended ajaxify_link() function below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
 * Function to add click event to elements.
 * Modifies browser URL and pulls content using XMLRequestObject.
 * Default click is disabled - thus browser is not redirected to href.
 */
function ajaxify_link(link_element) { 
 
  // Destroy any currently bound click events to this element.
  link_element.onclick = null;
 
  // IE8
  if (!link_element.addEventListener) {
    link_element.attachEvent("onclick", function(e) { 
      // Prevent default browser action on click.
      // This will stop the browser redirecting to  the HREF value.
      e.returnValue = false;
 
      // Change URL within browser address bar.
      changeBrowserURL(link_element);      
      
      // Get page and replace current content.
      getPage(link_element.href);
    }, false);
  }
  
  // FF, Chrome, Safari, IE9.
  else {
    link_element.addEventListener("click", function(e) { 
      // Prevent default browser action on click.
      // This will stop the browser redirecting to  the HREF value.
      e.preventDefault();
 
      // Change URL within browser address bar.
      changeBrowserURL(link_element);      
      
      // Get page and replace current content.
      getPage(link_element.href);
    }, false);
  }
}

If you now test this script, you will notice whilst navigating, the address bar is updated. If you are using a non-supporting browser (such as IE8), a hash is appended to the end of the URL with the corresponding page URL. As well as this, pressing the back and forward buttons on your browser updates the URL. This is great but you eagle eyed developers have realized that the page content is not being updated; only the URL within the address bar.

This occurrence is by design. Technically you have not been navigating through the site using URLs, but changing the state of the site by clicking on some links. In terms of the browser, all we have done is updated the URL with the address bar and added records to the browser history; the browser does not keep a record of what content was loaded and when, so the page is never re-rendered. The HTML5 API does however give us the opportunity to fix this scenario by using the event listener that goes by the name of popstate.

History API: popstate

The popstate event is triggered whenever the browser history is manipulated. In our case, this would trigger whenever we use the pushState() function.

Further to this, the 'state data' that is passed to pushState(), can be accessed by simply reading history.state. You may also want to note that this state is available even if the browser is refreshed, which lets you perform some logic depending what exists in the payload. Say for example, if you added some classes to a div when the user was clicking some navigation buttons - you could re-add them all after the browser refreshed without having to use an AJAX call.

In our example, we will be using the popstate event to re-fetch the pages whenever the user clicks on the back and forward buttons with the browser. This will work on browsers that support pushState(), so IE is out of the picture. We will work with the onhashchange event to get this working in non-compatible browsers, which is just a trigger for when a hash change is detected within the address bar.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
 * Function to detect when back and forward buttons clicked.
 *
 * This function will allow us to load content on the fly, as
 * the browser cannot re-render the AJAX content between state changes.
 */
function popStateHandler() {
  // FF, Chrome, Safari, IE9.
  if (history.pushState) {
    // Event listener to capture when user pressing the back and forward buttons within the browser.
    window.addEventListener("popstate", function(e) {
      // Get the URL from the address bar and fetch the page.
      getPage(document.URL);
    });
  }
  
  // IE8.
  else {
    // Event listener to cature address bar updates with hashes.
    window.attachEvent("onhashchange", function(e) {
      // Extract the hash
      location_url = window.location.hash;
 
      // Remove the # symbol.
      location_url = location_url.substring(1);
      
      // Fetch page using the constructed URL.
      getPage(location_url);
    });
  }
}

The events listeners we created attach to the window object, opposed to any other elements on the page. To ensure that we are listening for these changes as soon as the DOM is ready, we will attach these events at the beginning of the script. To do so, we will update the timer we created at the very start of the script.

amended DOM ready code.

1
2
3
4
5
6
7
8
9
10
11
12
// Timer to check when DOM is ready.
var readyStateCheckInterval = self.setInterval(function() {
  // Check if DOM ready.
  if (document.readyState === "complete") {
    // When DOM ready, destroy timer and execute setupLinks() function.
    clearInterval(readyStateCheckInterval);
    setupLinks();
    
    // Attach event listeners for browser history and hash changes.
    popStateHandler();
  }
}, 10);

Now with our fallback implementation using the onhashchange event, it is triggered whenever the hash is updated in the URL. Thus it is updated whenever the user pressed the back and forwards buttons and when a link is clicked. You can check this by placing an alert("Test") after each getPage() call for the fallback implementations. Since both events are firing, two alert boxes will popup when a link is clicked; the page is being fetched twice.

The fix is to just remove the getPage() function from the ajaxify_link() function. The browser will still be prevented from redirecting and the hash will be updated as normal. The onhashchange event will take care of fetching the page for both a user clicking the link and using the back and forward buttons.

Amended ajaxify_link function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
 * Function to add click event to elements.
 * Modifies browser URL and pulls content using XMLRequestObject.
 * Default click is disabled - thus browser is not redirected to href.
 */
function ajaxify_link(link_element) {
 
  // Destroy any currently bound click events to this element.
  link_element.onclick = null;
 
  // IE8
  if (!link_element.addEventListener) {
    link_element.attachEvent("onclick", function(e) {
      // Prevent default browser action on click.
      // This will stop the browser redirecting to  the HREF value.
      e.returnValue = false;
 
      // Change URL within browser address bar.
      changeBrowserURL(link_element);
    }, false);
  }
 
  // FF, Chrome, Safari, IE9.
  else {
    link_element.addEventListener("click", function(e) {
      // Prevent default browser action on click.
      // This will stop the browser redirecting to  the HREF value.
      e.preventDefault();
 
      // Change URL within browser address bar.
      changeBrowserURL(link_element);
 
      // Get page and replace current content.
      getPage(link_element.href);
    }, false);
  }
}

Final Script

The following is the final contents of the script.js file, repeated here for convenience.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// Timer to check when DOM is ready.
var readyStateCheckInterval = self.setInterval(function() {
  // Check if DOM ready.
  if (document.readyState === "complete") {
    // When DOM ready, destroy timer and execute setupLinks() function.
    clearInterval(readyStateCheckInterval);
    setupLinks();
    
    // Attach event listeners for browser history and hash changes.
    popStateHandler();
  }
}, 10);
 
/*
 * Function to pass reference of elements to ajaxify_link() function.
 * Any additional links should be added here.
 */
function setupLinks() {
  // Send elements off to have a click event added.
  ajaxify_link(document.getElementById("home-link"));
  ajaxify_link(document.getElementById("contact-link"));
  ajaxify_link(document.getElementById("about-link"));
}
 
/*
 * Function to add click event to elements.
 * Modifies browser URL and pulls content using XMLRequestObject.
 * Default click is disabled - thus browser is not redirected to href.
 */
function ajaxify_link(link_element) {
 
  // Destroy any currently bound click events to this element.
  link_element.onclick = null;
 
  // IE8
  if (!link_element.addEventListener) {
    link_element.attachEvent("onclick", function(e) {
      // Prevent default browser action on click.
      // This will stop the browser redirecting to  the HREF value.
      e.returnValue = false;
 
      // Change URL within browser address bar.
      changeBrowserURL(link_element);
 
      // Get page and replace current content.
      getPage(link_element.href);
    }, false);
  }
 
  // FF, Chrome, Safari, IE9.
  else {
    link_element.addEventListener("click", function(e) {
      // Prevent default browser action on click.
      // This will stop the browser redirecting to  the HREF value.
      e.preventDefault();
 
      // Change URL within browser address bar.
      changeBrowserURL(link_element);
 
      // Get page and replace current content.
      getPage(link_element.href);
    }, false);
  }
}
 
/*
 * Function to modify URL within browser address bar.
 */
function changeBrowserURL(dom_element) {
  // Change URL with browser address bar using the HTML5 History API.
  if (history.pushState) {
    // Parameters: data, page title, URL  
    history.pushState(null, null, dom_element.href);
  }
  // Fallback for non-supported browsers.
  else {
    document.location.hash = dom_element.getAttribute("href");
  }
}
 
/*
 * Function to fetch page via URL.
 */
function getPage(link_url) {
    // Get content using XMLHttpRequest object.
    XMLHttp = new XMLHttpRequest();
    
    // Set the request type, URL and disable asynchronous mode,
    // as we don't care about the script hanging waiting for the reply.
    XMLHttp.open("GET", link_url, false);
    
    // Send the request to the server.
    XMLHttp.send();
    
    // The response we receive is not in XML format. So we cannot 
    // use the DOM to extract what we need directly.
    // Thus we assign it to a new element first.
    var dom_response_holder = document.createElement('div');
    
    // Remember to use 'responseText' instead of 'response', as this won't work in IE.
    dom_response_holder.innerHTML = XMLHttp.responseText;
 
    // Now extract what we need using the DOM.
    var new_container_element = dom_response_holder.getElementsByTagName("DIV");
    
    // getElementsByTagName() returns an array. We are only interested in the first element.
    new_container_element = new_container_element[0].innerHTML;
        
    // Replace current content.
   document.getElementById("container").innerHTML = new_container_element;
 
    // Rebuild links, to ensure all links are bound correctly.
    setupLinks();
}
 
/*
 * Function to detect when back and forward buttons clicked.
 *
 * This function will allow us to load content on the fly, as
 * the browser cannot re-render the AJAX content between state changes.
 */
function popStateHandler() {
  // FF, Chrome, Safari, IE9.
  if (history.pushState) {
    // Event listener to capture when user pressing the back and forward buttons within the browser.
    window.addEventListener("popstate", function(e) {
      // Get the URL from the address bar and fetch the page.
      getPage(document.URL);
    });
  }
  
  // IE8.
  else {
    // Event listener to cature address bar updates with hashes.
    window.attachEvent("onhashchange", function(e) {
      // Extract the hash
      location_url = window.location.hash;
 
      // Remove the # symbol.
      location_url = location_url.substring(1);
      
      // Fetch page using the constructed URL.
      getPage(location_url);
    });
  }
}

Final Message

Running this code in any modern browser other than IE, you will notice the address bar is updated, as if the browser is redirecting to another page. You will also notice that you can use the back and forward buttons within your browser and as a result, each will be added as a separate line within your history window (CTRL+H in Firefox).

Using IE, you will notice the data is loaded dynamically along with the address bar containing the hash at the end of the URL. Unfortunately, the fallback does not handle the history in any way. No history will be made available with the History panel. There is no good fix available, so you will need to wait for IE10 for this one.

With both implementations; the user can share and bookmark with no problems.

There we have it, a mini site using AJAX to load data dynamically and making use of the HTML5 History API to update the address bar and log history correctly. As a fallback for non-supporting browsers, hashes are used. To top it all off, we developed all of this without the use of any external libraries. We’ve just made History.



Our Partners / Accreditations / Networks

0161 872 3455

Sign up to our newsletter