Advertisement
HTML & CSS

A First Look at the HTML5 History API

by

HTML5 introduces a variety of new goodies for front-end developers, such as the additions to the browser's history object. Let's take a look at its new features in this lesson.


Introduction

Always present the same information when the user refreshes the page.

The history object isn't new; in fact, you can trace its beginnings to the early browsers from the 1990s. While it has never been based on a public standard, until HTML5 that is, every browser has supported its meager, yet sometimes useful, functionality. Since its inception, the history object has provided a means to work with the history of a particular tab in the browser (or a window before tabbed browsing became the norm). This is sometimes referred to as session history.

The old history object gave us the ability to programmatically navigate backwards and forwards, the equivalent of the user clicking the Back and Forward buttons. But HTML5 finally updates the history API by adding the ability to manipulate the browser's URL and maintain state; although URL manipulation, to some extent, has been possible since the introduction of the location object. For example:

//On the URL: http://net.tutsplus.com/

//Get the full URL
location.href //"http://net.tutsplus.com/"

//Get the hash fragment
location.hash // "" (an empty string)

//Set the hash fragment
location.hash = "hello"
 
//Get the full URL
location.href //"http://net.tutsplus.com/#hello"

//Get the hash fragment
location.hash //"#hello"

The native API is easy enough to use...

It's common for some web applications to use "hash-bangs" (#!). Not only can they prevent the browser from navigating to a different page (making dynamic web pages easier to manage), but they can also aid in search engine optimization (SEO) (until recently, Twitter made extensive use of hash-bangs).

The hash-bang technique is useful when you have a lot of content that you want to display in the same page while allowing users to bookmark certain parts of a page. You can also use hash-bangs in conjunction with infinite scrolling scripts keep track of the user's position by storing that information in the URL.

The technique is simple: store information in the URL, parse it, and then use Ajax to load content. It sounds wonderful, but there are many reasons not to use this technique. To summarize:

  • A URL like http://domain.com/#!1234 may fail to load correctly if JavaScript is not enabled.
  • You may have two different URLs pointing to the exact same content (eg: http://domain.com/#!1234 and http://domain.com/1234)--a no-no for SEO.
  • The server is unaware of fragment identifiers.

Using the History API

The History API helps solve the aforementioned issues by giving us the ability to transform URLs, like http://domain.com to http://domain.com/hello, without triggering a page refresh. The following lists the history object's members and their purposes:

  • history.back(): Navigates to the previous URL in the history stack.
  • history.forward(): Navigates to the next URL in the history stack.
  • history.go(): Navigates to the URL at the specified index in the history stack. e.g. history.go(-2)
  • history.pushState(): Adds a URL to the history stack with a specified state. e.g. history.pushState({ foo : "bar"}, "New title", "new-url.html"), where the first argument is a state object.
  • history.replaceState(): Updates (rather than adds) the current URL on the history stack with the provided state information. e.g. history.replaceState({ foo : "bar"}, "New title", location.href)
  • history.length: Returns the amount of URLs in the history stack.
  • history.state: Returns the state object at the top of the history stack.

The following example uses no external libraries:

<nav>
  <ul>
    <li><a href="/history/example/index.html">Home</a></li>
    <li><a href="/history/example/hello.html">hello</a></li>    
    <li><a href="/history/example/about/index.html">About</a></li>
  </ul>
</nav>
// NodeLists do not have a forEach method
[].forEach.call(document.querySelectorAll("nav a"),function(e) {
	e.addEventListener("click", function(evt) {
	  var title = this.textContent;
	  var url = this.href;

	  //Change the URL
	  history.pushState(null, title, url);

	  //Do some ajax stuff

	  //Prevent the browsers default behaviour of navigating to the hyperlink
	  evt.preventDefault();
	})
});

The native API is easy enough to use, but you can find many libraries that greatly help with the common patterns of intercepting a link, loading data via Ajax, and inserting the data into the page. Two popular libraries are pjax and History.js.


With Great Power...

As with any technology or API, be mindful of best practices. Let's look at a few best practices when using the history API.

Be Kind to URLs

Don't change the URL just because you can; only change it when it makes sense to do so!

For example, let's say your online shop resides at https://shop.domain.com/, and the homepage displays a list of popular items. Clicking on one of the items could open a modal window that contains the product's information (retrieved via Ajax, of course). You wouldn't need to change the URL when doing this; instead, you could provide a link to the product in the modal window that would take the user to the product's page.

Another example would be to maintain the user's scrolling position in an infinite scrolling situation. The user could refresh the page and continue where they left off.

The Chrome Web Store changes its URL when showing different items. No page refresh occurs when going from /webstore/category/popular to /webstore/detail/pjkljhegncpnkpknbcohdijeoejaedia.

Ensure Compatibility

Only load what is necessary.

Unfortunately, older browsers do not support pushState() and replaceState(). Therefore, it is important to ensure that both your page and the user experience (UX) are not broken in those browsers. Use the tried and true concept of progressive enhancement.

A common use case might be to intercept a link's click event and use Ajax to load the content in a new window while also changing the URL. Make sure that the links work normally without JavaScript; the anchor element should have a valid URL in the href attribute. Then for browsers that do support the new goodies, the JavaScript code would retrieve the URL's content via Ajax. Here's what that code might look like:

//Using jQuery (would work fine with raw javascript)
if ("pushState" in history) {
    $("nav a").on("click", function() {
        history.pushState(null, this.textContent, this.href);
        return false;
    });​
}

Don't Download Unnecessary Markup

Ajax is wonderful, and it can be tempting to take the easy road and download an entire HTML document to display in a modal window. Don't do that! Downloading unnecessary data can take its toll on the UX, especially on slow connections.

Take the extra time to ensure your application doesn't waste bytes over the wire, even if it means spending extra time on your back-end code. You can send a custom HTTP header that indicates that the server should only serve minimal content (e.g. JSON, HTML fragments, etc).

/* Note server support for HTTP_X_REQUESTED_WITH may vary, also it may be worth sending your own custom header in the case that the JavaScript doesn't send the header you expected */
if(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
    //Ajax request
    echo "Content without lots of crazy markup";
}
else {
    //Non-ajax request
    include('header.php');
    ...
}

The demo demonstrates this. Click on cat.php on Example 4, and you'll notice only the necessary content is sent through in the response.

Maintain Continuity

Be mindful of best practices.

Always present the same information when the user refreshes the page. It might sound obvious, but it's easy to forget when dealing with so much client-side code.

When you change a URL via pushState() from http://domain.com to http://domain.com/contact, your web server may look for a directory called contact or a file named contact.html. It's important that URLs you use with the history API should be actual URLs that your server responds to.

There are many frameworks that can handle routing for you; it's worth using one if you are able to.

Github uses pushState() for semantically different portions of content; their content looks the same when you refresh the page.

Sensibly Handle the Forward/Back Buttons

Ensure that both your page and the user experience (UX) are not broken in those browsers.

A popstate event fires every time the current history entry changes. Use this event for a consistent UX.

For example, let's assume you use Ajax to load content with your team's members. The URL might look like http://domain.com/team/person1. The user then clicks on the "Next" link in the UI which loads Person 2's information (http://domain.com/team/person2). If the user then clicks the browser's Back button, Person 1's information may not automatically load. It's up to you to retrieve the state information and display Person 1's information.

Once again, be sure to only load what is necessary. If Person 1's information is already loaded, you don't need to request it again. Show and hide DOM elements when necessary. You can also pass state objects to pushState() to maintain state.

<article data-person-id="1234">...</article>
var personId = '1234';
/*
* jQuery selector
* If the html fragment for personId is found, we should show it
*/
var person = $(".person[data-person-id="+personId+"]");
if ( person && person.length > 0 ) {
	person.show();
}

The ever informative caniuse.com site has a great implementation (hash fragments aside!) for handling the Back button. Try it out. Edit some text in the search field and click Back. Hint: Notice the update delay with the address bar. This delay prevents constant updates to the URL with every key press, as opposed to less frequent updates when you finish entering a search term.

Use pushState and replaceState Appropriately

The pushState() method adds an entry to the history stack; whereas, replaceState() replaces the current entry. For example, let's assume you modify the URL with every keystroke the user makes in a text box. "Pushing" a new state adds an entry to the history each time the user submits that data; this isn't be the best solution because the user will need to click the Back button for each letter entry. Use replaceState() instead, like this:

<input type="text" id="search" />
$("#search").keyup(function() {
  history.replaceState(null, null, "search?=" + $("#search").val());
});

Further Reading

Naturally, this is just the tip of the iceberg. There are many techniques, patterns, and libraries on the web that work with the HTML5 history API. While I cannot possibly cover every aspect of the history API, I can provide you with a variety of resources to further your knowledge.

Related Posts
  • Code
    HTML5
    HTML5: Vibration APIPdl54 preview image@2x
    HTML5 has been a breath of fresh air for the web, which hasn't only affected the web as we know it. HTML5 provides a number of APIs that enable developers to create interactive websites and improve the user experience on mobile devices. In this article, we'll take a closer look at the Vibration API.Read More…
  • Web Design
    UX
    Walk Users Through Your Website With Bootstrap TourTour retina
    When you have a web application which requires some getting used to from your users, a walkthrough of the interface is in order. Creating a walkthrough directly on top of the interface makes things very clear, so that's what we're going to build, using Bootstrap Tour.Read More…
  • Code
    JavaScript & AJAX
    Getting Into Ember.js: Part 5Getting into ember
    Editor's Note: The Ember.js team has shifted to an expedited release schedule and as of this publication date are on version 1.2.0. This tutorial was written pre-v1.0 but many of the concepts are still applicable. We do our best to commission timely content and these situations happen from time-to-time. We'll work to update this in the future. In part 3 of my Ember series, I showed you how you can interact with data using Ember's Ember.Object main base class to create objects that define the methods and properties that act as a wrapper for your data. Here's an example:Read More…
  • Code
    Tools & Tips
    Tips to Avoid Brittle UI TestsUi test retina preview
    In the last article I talked about a few ideas and patterns, like the Page Object pattern, that help write maintainable UI tests. In this article we are going to discuss a few advanced topics that could help you write more robust tests, and troubleshoot them when they fail:Read More…
  • Code
    PhoneGap
    PhoneGap: Build a Feed Reader - Project StructurePhonegap feed reader@2x
    Although not specifically created to work together, jQuery Mobile and Cordova (also known as PhoneGap) are a very powerful duo to create hybrid, mobile apps. This series will teach you how to develop a feed reader using web technologies and these two frameworks. Over the course of this series, you'll also become familiar with the Cordova Connection and Storage Core Plugins and the Google Feed API.Read More…
  • Code
    Ruby
    Digging Into Rails 4Digging rails
    Rails 4 is rapidly approaching. In this article, let's take a look at some of the new features that it offers, as well as the changes that may affect your current applications.Read More…