In the world of web-apps we commonly create our applications as “single-page websites”. That is, the page never reloads – we opt instead to manipulate the DOM based on user input to give the look of a seamless native application. Frequently we may need to have the ability to go “back” to the previous view that the user was on. This might be because the UI contains a “Back” button or maybe we’re on Android and want to enable the software back button.

It might seem trivial to just hard-code a back-button but its common to have templated views capable of displaying many different kinds of information and simply going “back” ignores the state of the view. You may think that all you need to do is to declare a variable somewhere that holds a string or object that tells you what your previous view contained and then branch off of that. True, that would work – how about going back multiple steps? Again, you can hack something together I’m sure….

At some point in the past someone was thinking about the same thing and hit upon the idea that the browser’s built-in history object can be used to track an application’s view history. As our app is a single-page application we aren’t loading completely new pages so the history object will just “sit” on the current page/url. Its possible however to update the history object without loading a new page by using “anchor references” which become part of the page’s history but do not cause a page reload.

For example, we can change this:

  • index.html

To this:

  • index.html#hello

An anchor is meant as a quick way to jump down to a specific part of a long page, thus its name. In our case, its counterpart does not exist on the page so merely changing the url of our application to include an anchor does nothing other than to update the page’s history object. When applied to our purposes the anchor will have a special meaning and so moving forward we’ll call it by its technical name – a hash fragment or just hash for short.

Knowing that the page’s history object includes hashes might not seem very interesting. Consider though that there is an event called window.onhashchange that is fired every time the hash changes. Consider next that we have a method of updating the hash in the form of window.location.hash. Further that we can use window.history.back() to traverse the history object which triggers the window.onhashchange and we soon realize that we have the essential ingredients to create a hash-based view router: 1) a method of keeping track of views, 2) a method of going back in our view history, 3) a method of knowing when the hash changes, and 4) a method of changing the hash.

You may have noted that the title of the article contains the word “Hashbang” – this is what a hashbang looks like: #!

The purpose of the “bang” (the exclamation point) is to function as a flag to Google’s bots that the URL they have just parsed should be considered as a link, not an anchor to something else lower on the page. If we use hashbangs in our single-page applications on the web Google will know that its an *application* and not just a single page and will index our links and treat them as individual pages.

For clarity’s sake, here are some hashbang examples:

  • index.html#!home
  • index.html#!products
  • index.html#!productDetail

For web-apps the hashbang isn’t as useful but its part of how we do things none-the-less. You’ll see HTML5 frameworks all over use them as they don’t know if your intent is to build a web-app for a mobile device or to build a single-page app that will live on the internet. As a result its baked in as a hashbang instead of just a hash and so as I said its just how things are done. Contrary to what my mother says I’ve decided that just because everyone else does it that it does in fact mean that I should do it too.

However, if you’ve read what html5doctor.com has to say on the topic you would walk away with a completely different perception:

You may have already seen articles fussing over the adoption of the “hashbang” (#!) pattern on sites like Twitter. This technique updates the address bar with a fragment identifier that can then be used by JavaScript to determine which page and state should be displayed.

This works as a method of creating a bookmarkable, shareable URL for a page’s state in the absence of a standard API.

…It’s ugly. It’s a hack and it looks like one…

…The hashbang was never intended to be a long-term solution, so don’t rely on it….
~ html5doctor.com

“so don’t rely on it”…. the exclamation point’s presence is by the whim of the developer or baked in by a framework. Its presence or lack thereof is actually immaterial to our purposes – if you elect not to use it you still have a fragment identifier that can serve as a “bookmarkable, shareable URL for a page’s state”. A hashbang’s only use as I understand it is to help Google index our one-page websites instead of giving Google a “locked door” that it’s bots can’t get past.

According to Google:

…AJAX URLs containing hash fragments with #! are eligible to be crawled and indexed by the search engine.
~ Google, via “Making AJAX Applications Crawlable

It is true that this Google-ism isn’t part of the official HTML5 spec – nor should it be as it has nothing to do with HTML5. As the hashbang is used more often than not by various frameworks, Twitter, Bing, and other web-properties its repeated use for this particular purpose amounts to a de facto standard for ***indexing AJAX-based single-page applications*** though admittedly not one that is part of any “official” standard.

The inclusion of the “bang” on the topic of Hash-based View Routing is more convention than anything else. And the convention is that “hash” and “bang” go together – use it or not, its up to you. If you use it within your app I’m pretty sure you can “rely on it”.

Sample App

The code presented here is functional and is based on a mythical Audi hybrid app. Click the image to view the app which contains 4 views used to illustrate the topic of this article.

Methodology

This is how I handled tracking view state – it makes sense to me and definitely fits the jQuery way of doing things with data locked into DOM elements via data attributes. Other frameworks like backbone.js or angular.js provide an abstraction for managing view history. I wanted to get past the abstraction that a framework provides and see about doing it myself.

Triggering a view change

Here’s the run-down on triggering a view change:

  1. A button is touched – the associated event object and its data attributes are broadcast to all subscribers of the touchend event
  2. The triggerViewChange method looks at the “action” data attribute – if it equals “updateHash” then the the event object is pushed to the _events array for future use and the page’s hash is set via window.location.hash (_events is described in more detail later in this article)
  3. Setting the hash itself causes the hashchange to fire
  4. notifyHashChange is listening for hashchange events and now receives the event and notifies the methods that have subscribed to it. Review the first code sample provided later on this page – the init() method – there are four functions subscribed to the hashchange event and they all create views. Those functions are:
    • audi.controller.showDefaultView()
    • audi.controller.showModelsView()
    • audi.controller.showModelsDrillDownView()
    • audi.controller.showCarDetailView()
  5. Each of the functions subscribed to the hashchange inspects the window’s hash – if it corresponds to a specific key then the function knows it is the view that must be rendered to the screen
    • Recall that the event object that triggered all of this was added to the end of the _events array – when a view changes it inspects that last item of this array for the desired event object and inspects the HTML5 data attributes it needs in order to know how to configure the content.

That’s it, a new view has been triggered and is now displayed, we’ve updated the _events array and the windows’s location object has a record of where we went.

Going back in the view history

You can simulate clicking a button by using Chrome’s console and entering “window.history.back()” provided that you created a history to begin with by touching the different navigation buttons.

Here’s the rundown on what happens when window.history.back() is invoked by hand via the console or by an in-app “back” button:

  1. showPreviousView() is triggered – this function will:
    • Remove the last array item from _events
    • window.history.back() is invoked
  2. The hashchange event is fired and everything wired to it does its thing:
    • The hash is now different – the four functions subscribed to this event are notified and each inspect the hash
    • The function whose key matches the hash fires and the appropriate view is rendered
    • As before the view in question looks at the last item in the _events array. Since we removed one from the end of the array (step 2 above) the last array item is now in-synch with our view. Everything the view needs to know about this prior view is now available to it and so it configures its content appropriately

Methodology in Practice

What I’m going to show here uses my own personal convention for building apps. To get a refresher for how I approach things you should first read “The Pub/Sub Pattern and Event Delegation in jQuery“.

Lets skip all of the setup – assuming you read the aforementioned article we will start with a bare-bones app – 4 views/pages – here’s all the code:

A short intro to the code is below.

Init the app (audi.js)

To initialize the app I have the following in the index.html

<script>
	$(function(){
		//document.addEventListener('deviceready', audi.init, false);
		audi.init();
	});
</script>

As you can see this is the hook to get things started. Once the document’s assets have been loaded jQuery’s ready event fires which in turn fires the app’s init() function.

init() sets up our controller by associating functions with the proper touch events and then by setting up all the listeners required by the application.

;(function(ns,$){
	/* init()
	 * This inits the application.
	 * @type {Function}
	 * @param {}
	 * @return {} Returns nothing
	 */
	ns.init = function(){
	audi.controller.subscribe('touchend',audi.controller.doButtonTouched);
	audi.controller.subscribe('touchend',audi.controller.doButton2Touched);
	audi.controller.subscribe('touchend',audi.controller.triggerViewChange);
        audi.controller.subscribe('touchend',audi.controller.showPreviousView);

        audi.controller.subscribe('hashchange',audi.controller.showDefaultView);
        audi.controller.subscribe('hashchange',audi.controller.showModelsView);
        audi.controller.subscribe('hashchange',audi.controller.showModelsDrillDownView);
        audi.controller.subscribe('hashchange',audi.controller.showCarDetailView);

	$('body').on('touchend','div',audi.controller.notify);
        $(window).on('hashchange',audi.controller.notifyHashChange);
        $(window).on('popstate',audi.util.showHideBackButton);

	window.location.hash = ' '; // make sure the hashchange event fires if the page is reloaded, useful in desktop browsers
        window.location.hash = 'defaultView';

	}
})(this.audi = this.audi || {},jQuery);

Of special note to us are lines 11 & 12 which assign our triggerViewChange and showPreviousView as subscribers to the touchend event. The buttons that exist in this sample application can do one of two things, either 1) change the view (move forward) or 2) go to the previous view (move backwards).

Another thing to note are lines 20 & 21.

Line 20 contains the setup for the hashchange listener. Every time the hash is updated the method “listening” to this event is fired. The 2 lines previously mentioned are responsible for changing the hash – again, they were triggerViewChange and showPreviousView.

Line 21 sets up a listener to the popstate event – I’m using this merely to control when my in-app back button should appear. You can view the source inside of audi.util.js to learn more.

The Controller

Below is a fragment of the controller code showing only the points of interest to our topic.

/** 
 * audi.controller 
 * @type {Object}
 * @return {} returns nothing
 * @name audi.controller
 * @namespace holds the event pub/sub system and button methods
 */
;(function(ns,$){

    var _uiUpdatePause = 5;
    var _subscriptions = {};
    var _events = []; 

	/* subscribe()
	 * This handles the event subscriptions.
	 * @type {Function}
	 * @param {string} eType - the event type
	 * @param {object} cb - the function reference
	 * @return {} Returns nothing
	 * @see audi.init()
	 */
	ns.subscribe = function(eType,cb){
		if (!_subscriptions.hasOwnProperty(eType)){
			_subscriptions[eType] = [];
		}
		_subscriptions[eType].push(cb);
	};

	/* notify()
	 * This notifies the event subscribers (hashchange has a separate publisher).
	 * @type {Function}
	 * @param {object} e - the event object
	 * @return {} Returns nothing
	 * @see audi.init()
	 */
	ns.notify = function(e){
		if (!$(e.target).data('action')){
			return;
		}
                if (audi.util.getIsScrolling()){
                       return;  
                }
		e.preventDefault();
		e.stopPropagation();

		var cbs = _subscriptions[e.type];
		for (var i=0;i<cbs.length;i++){
			cbs[i](e);
		}
	};

	/* notifyHashChange()
	 * This notifies the hashchange event subscribers.
	 * @type {Function}
	 * @param {object} e - the event object
	 * @return {} Returns nothing
	 * @see audi.init()
	 */
        ns.notifyHashChange = function(e){
            var cbs = _subscriptions[e.type];
		for (var i=0;i<cbs.length;i++){
			cbs[i](_events.slice(-1)[0]);
		}
        }

	/* triggerViewChange()
	 * Changes the location hash which will trigger the hashchange 
         * event which itself triggers the appropriate view update.
	 * @type {Function}
	 * @param {object} e - the event object
	 * @return {} Returns nothing
	 * @see audi.init()
	 */
	ns.triggerViewChange = function(e){
		if ($(e.target).data('action') != 'updateHash'){
			return;
		}
		var hash = $(e.target).data('hash');
		_events.push(e);
		setTimeout(function(ev){
			window.location.hash = hash;
		},_uiUpdatePause);
    	}
	/* showPreviousView()
	 * This is called by a hashchange event - it uses the window object's
         * history.back method to go to the previous view.
	 * @type {Function}
	 * @param {object} e - the event object
	 * @return {} Returns nothing
	 * @see audi.init()
	 */
	ns.showPreviousView = function(e){
		if ($(e.target).data('action') != 'showPreviousView'){
			return;
		}
        	var hash = window.location.hash;
        	if (hash == '#defaultView' || hash == 'modelDrillDownView'){
			return;   
		}
        	_events.pop(); // remove the current recorded event so that the previous one will be used.
		window.history.back();
	}

	/* showDefaultView()
	 * Handles the click event for the logo, this renders the default view.
	 * @type {Function}
	 * @param {object} e - the event object
	 * @return {} Returns nothing
	 * @see audi.init()
	 */
	ns.showDefaultView = function(e){
		if (window.location.hash !=  '#defaultView'){
			return;
		}
		audi.view.destroy_iScroll();
        	audi.view.renderDefaultView(_events.slice(-1)[0]);
	}

	/* showModelsView()
	 * Handles the click event for the models button.
	 * @type {Function}
	 * @param {object} e - the event object
	 * @return {} Returns nothing
	 * @see audi.init()
	 */
	ns.showModelsView = function(e){
		if (window.location.hash !=  '#modelsView'){
			return;
		}
		audi.view.destroy_iScroll();
        	$('#mainView').removeClass('splashBG');
        	audi.view.renderShowModels(_events.slice(-1)[0]);
	}

	/* showModelsDrillDownView()
	 * Handles the click event for the models button.
	 * @type {Function}
	 * @param {object} e - the event object
	 * @return {} Returns nothing
	 * @see audi.init()
	 */
	ns.showModelsDrillDownView = function(e){
		if (window.location.hash !=  '#modelDrillDownView'){
			return;
		}
		audi.view.destroy_iScroll();
        	audi.view.renderDrillDown(_events.slice(-1)[0]);
	}

	/* showCarDetailView()
	 * Handles the click event for the models button.
	 * @type {Function}
	 * @param {object} e - the event object
	 * @return {} Returns nothing
	 * @see audi.init()
	 */
	ns.showCarDetailView = function(e){
		if (window.location.hash !=  '#showCarDetailView'){
			return;
		}
		audi.view.destroy_iScroll();
		audi.view.renderCarDetail(_events.slice(-1)[0]);
	}
...

What is the “_events” variable used for?

You’ll see on line 12 above that I’ve declared a variable called _events and its purpose is to collect touch events in an array as they occur. Not all touch events, only ones that are “actionable” as determined by HTML 5 data attributes within some DOM elements (data-action or your own convention). Specific elements are in fact buttons and trigger view changes – and thus hashchange events. These buttons contain information on the view itself and so I hold the touchend events from our button divs (and thus their data attributes) in this array so that it can be passed to the relevant views when the hashchange event fires. Thus, if I go back in my browser history – effectively going backwards in my view history – I have the event that was used to create that previous view and by extension all of the information necessary to populate that prior view.

This _events array is managed by triggerViewChange() which pushes new event objects to the array and showPreviousView() which removes the last array item when the hashchange event fires.

The current event object is accessed like so: _events.slice(-1)[0]. Thus we are able to manage views via hash changes while feeding the views relevant data per the dom element that triggered the hashchange to begin with.

Of course we could choose instead to keep all the DOM-locked data attributes as part of our hash, something like this:

  • index.html#foo=bar&dog=brown&bacon=awesome

I didn’t use that approach because there’s no need as it adds complexity where its not needed. Did you notice how little effort was required to go back in our app’s history? By saving the touch event object I already have all the data represented in the above example. Everything is much simpler and requires less code without the need to parse hash fragments.

Of course, if you have a single-page web page (not a hybrid app) and need bookmarking support or deep linking then you should consider a hash fragment that contains name/value pairs representing whatever state your view requires.

Summary

Once you wrap your brain around the ideas here you realize its not so difficult. The “ah-ha” moment should have happened after you’ve read the Methodology section. If not and you’ve read this far go back and re-read it now that you have the code in your brain as a reference.

Download the entire working example. Note that it uses touch events so you have to enable them in your modern browser of choice.

Also, you can give this a whirl in PhoneGap too if you like, just package up the contents and do a build. You may want to edit the index.html so that the app builds its initial view on “device ready”.