New Bamboo Web Development

Bamboo blog. Our thoughts on web technology.


Degradable JavaScript Applications Using HTML5 pushState

over 2 years ago by Oliver

What is the problem?

Using the location hash to keep track of current page state and enable back button navigation is more and more common with large, full featured, client side JavaScript apps. Whilst the behaviour this gives is definitely an improvement to the user experience, implementing this with the location hash has some shortcomings.

Thankfully, as with everything else on the web, HTML5 is here to solve all your problems, with two methods and an event, pushState, replaceState & onpopstate.

What is pushState?

pushState & replaceState

The history object gains two new methods, pushState & replaceState, that allow us to change the current location of the browser without making an HTTP request. These new locations are stored in the browsers history so that both the back and forward button work as expected.

1 history.pushState({foo: "bar"}, "foo page", "/foo")

If you have a modern browser you can try out the above code in the console. You should see the url bar change to http://blog.new-bamboo.co.uk/foo. The browser doesn't check whether that url exists on the server, it does't even make an HTTP request. You'll notice that if you press the back button the url will change back, and pressing forward will set it to http://blog.new-bamboo.co.uk/foo again.

The other method, replaceState, is very similar but instead of pushing a new state into the browsers history it replaces the current state (as the name suggests).

1 history.replaceState({foo: "baz"}, "bar page", "/bar")

Both methods take three params, a state object (more about this later), a title and a url. The title is currently ignored by browsers, my guess is that at some point it will set the window title. The url param is what is used to set the current location in the browsers url bar. It is an optional parameter and if omitted the current location will be used. You can read more about these methods at MDC.

onpopstate

The onpopstate event fires whenever the browser location changes from the use of the back or forward button being used, regardless of whether pushState or replaceState were used to set the current location.

1 window.onpopstate = function (event) {
2   // see what is available in the event object
3   console.log(event)
4 }

Used in combination with the pushState & replaceState it can be used to handle changes in the browsers location. If the popstate event is fired as a result of navigating to a point in history that was added with either pushState or replaceState then the state object passed when calling those methods will be available as event.state.

Unfortunately there is no native onpushstate event, so to be notified of when something is pushed onto the history you will have to roll your own. Again to get more details about this event head over to MDC.

pushState vs location.hash

pushState has some considerable advantages over location.hash for the purpose of not breaking the back button. Firstly, although it is now part of the HTML5 spec, the onhashchange event is not supported everywhere, and until recently checking for changes to the hash required polling.

Also, as the name suggests, the onhashchange event only fires when the hash changes, it is not possible to detect when the same location is navigated to consecutively. With push state you can push the same location onto the browser history as many times as you wish and onpopstate will fire for each of these locations.

The biggest short comings of using the location hash are that the url anchor is a purely client side piece of information. When navigating to a location with an anchor the server is never sent the hash proportion of the location. This can cause issues when JavaScript is not available to provide the routing based on the hash location.

Since the whole url can be changed when using pushState, any time that url is navigated to without JavaScript available, the server gets the full location. This allows a server to respond with the full request.

Davis.js vs Sammy.js

Here at New Bamboo we have made full use of Sammy.js for over a year now for routing in JavaScript applications and it has served us well. Sammy.js uses location.hash for its routing so that you can use links with an anchor to control page state.

1 <a href="#/hello/bob">Say Hello to Bob</a>
2 <a href="#/hello/dave">Say Hello to Dave</a>
3 
4 var app = Sammy(function () {
5   this.get('#/hello/:name', function (ctx) {
6     alert('Hello ' + ctx.params['name'])
7   })
8 })

The above example shows a very simple Sammy app. When clicking on the links the location hash changes and the route is run, displaying a friendly greeting. This works brilliantly when JavaScript is available, however if the links are clicked and there is no JavaScript then nothing will happen. The server wouldn't know anything about our intention to greet either Bob or Dave.

I created a small library called Davis.js, which is heavily influenced by Sammy, and that uses pushState rather than location hash for its routing, the above example using Davis looks like this:

1 <a href="/hello/bob">Say hello Bob</a>
2 <a href="/hello/dave">Say hello Dave</a>
3 
4 var app = Davis(function () {
5   this.get('/hello/:name', function (req) {
6     alert('Hello ' + req.params['name'])
7   })
8 })

As you can see the two are almost identical, the only difference is the href of the links. If JavaScript is unavailable with the Davis example, then clicking on the links will make a regular request to the server. As long as the server can also respond to the /hello/:name route then the user will still get their friendly greeting.

Davis really is just a simplified version of Sammy using HTML5 pushState - be sure to check out a slightly more complex example and the docs.

Reuseable templates for dryer code

Where Davis really shines is in combination with a templating system that can be shared between both the client and the server. A good choice for this would be Mustache templates. A simple greeting template might look like this.

1 <h1>Hello {{name}}</h1>

If both the server and the client use this exact same template to respond to the /hello/:name route then it is relatively simple to make our simple app degrade gracefully when JavaScript is unavailable. So our JavaScript route might look like this:

1 this.get('/hello/:name', function (req) {
2   var html = Mustache.to_html("<h1>Hello {{name}}</h1>", {name: req.params['name']});
3 })

And the server, using Sinatra for example, could use the same template and do the same thing.

1 get '/hello/:name' do
2   Mustache.render("<h1>Hello {{name}}</h1>", {:name => params[:name]})
3 end

Mustache is currently very easy to integrate into Sinatra apps, however it isn't as straightforward with a Rails app. With the help of Mark Evans I have put together a simple gem called Poirot that enables use of Mustache partials within a Rails 3 app.

 1 # in greetings controller
 2 
 3 def show
 4   @name = params[:name]
 5 end
 6 
 7 # in app/views/greetings/show.html.erb
 8 
 9 <!-- to include the template for JavaScript to share -->
10 <%= template_include_tag "greeting" %>
11 
12 <%= render :partial => "greeting" %>
13 
14 # in app/views/greetings/_greeting.html.mustache
15 <h1>Hello {{name}}</h1>

The gem gives the templates access to all the instance variables defined in the controller and the normal Rails view helpers. A default view class is created but if you need more control over what variables are available in the Mustache template you can extend it.

 1 # in app/views/greetings/greeting.rb
 2 
 3 module Greetings
 4   class Greeting < Poirot::View
 5     def time
 6       Time.now
 7     end
 8   end
 9 end

The greeting mustache template would now also have access to the current time.

Conclusion

Using HTML5 pushState in combination with re-useable templates can provide a good starting point for more degradable and accessible JavaScript applications. Support for pushState is good in all good modern browsers, it is already being used by GitHub and Flickr, so the next time you need some kind of back button support in JavaScript apps why not try it out.