Skip to content

Intelligent State Handling

balupton edited this page Feb 11, 2011 · 35 revisions

Everyone seems to use the hashbang (#!) in URLs now... despite google recommending it - it is a ridiculously bad solution.

Why Hashbangs Suck

  1. Google is the only search engine which supports it.
  2. JS-Disabled users who access a hashed url won't have the right page
  3. There becomes with two urls for the same content - e.g. http://twitter.com/balupton and http://twitter.com/#!/balupton
  4. The url's gets polluted if we did not start on the home page - e.g. http://www.facebook.com/balupton#!/balupton?sk=info
  5. It is likely that the effort won't be put in to make the site accessible to non-javascript users and other search engines - e.g. forcing people to use http://twitter.com/#!/balupton instead of http://twitter.com/balupton
  6. It is likely that custom controller actions will be coded to handle the ajax requests specifically - rather than AJAXing the original urls and returning just the appropriate content (the template and any data, not the full page).

Solving the Problems

Problems #1 and #5 are solved by coding your website normally (without any server-side ajax support), hooking into the hashchange, and querying the non-hashed version of the new hashed url:

var $content = $('#content');
// hook into http://mysite.com/#/page
$(window).bind('hashchange',function(){
	// ajax request http://mysite.com/page
	var url = document.location.protocol+'//'+(document.location.hostname||document.location.host)+'/'+document.location.hash;
	$.get(url,function(data){
		// find content in the page's html, and apply it to our current page's content
		$content.html($(data).find('#content'));
	}
}

The solution above is a great improvement, though it still suffers from the other problems. The problem of #6 would be the next logical step, as the current solution causes a lot of overhead (we just need the updated content, not the entire page), so let's solve that now with the following server-side code:

<?php
	// Our Page Action
	public function pageAction ( ) {
		// Prepare our variables for our view
		// ...
		
		// Handle our view
		return $this->render('page.html');
	}
	
	// Render Helper
	public function render ( $template ) {
		// Get the full template path
		$template_path = ...
		
		// Render the template
		$template_html = file_get_contents($template_path);
		
		// Check for the XHR header
		if ( IS_XHR ) {
			// We are a AJAX Request, return just the template
			$this->sendJson({'content':$template_html});
		}
		else {
			// Wrap the Template HTML with the Layout and proceed as normal
			// ...
		}
		
		// Done
	}

In fact, jQuery Ajaxy has supported these solutions for problems #1, #5 and #6 out of the box since July 2008. There is also a Zend Framework Action Helper to make solving problem #6 easier.

Though, this still suffers from the other problems. This is where the HTML5 History API comes to the rescue, as without that it is not possible to solving those problems. The HTML5 History API allows users to modify the URL directly, attach data and titles to it, without changing the page! Yay.

That's great, though each HTML5 Browser (Safari, Chrome and Firefox 4) all support it differently, so if you just use the native API available to you, you're going to come across a series of bugs. History.js has been made to provide a cross-compatible solution for the HTML5 browsers, as well as supporting HTML4 browsers by a hash (#) fallback (optional as of v1.5). We do not use a hashbang (#!) fallback as it would be useless as our website is already indexable by search engines.

So what would the ultimate solution look like that solves all these problems?

var $content = $('#content');
// hook into http://mysite.com/#/page
$(window).bind('statechange',function(){
	// ajax request http://mysite.com/page
	$.get(History.getState().url,function(data){
		// find content in the page's html, and apply it to our current page's content
		$content.html($(data).find('#content'));
	}
}

And bang you have just solved 5 out of 6 of all our problems. Problem 6 requires a server side effort - plugins are currently being made for the every darn CMS and Framework.

Notes

Okay, so that sounds too good to be true. What do we need to be aware of?

  • If you choose to support HTML4 browsers by using a hash fallback using History.js the use case of "A HTML4 JS-enabled user shares a url with a JS-disabled user, the url does not work." - the only way to solve this is by just supporting states in HTML5 browsers and not HTML4 browsers - say not using the hash fallback at all.
  • If you have multiple areas on your website that change (more than just one div called #content), or if you want to utilise multiple levels of ajaxable pages (e.g. pages as well as sub-pages) for further optimisation and a better experience then this requires some more server side work. Currently there is only the HTML4 jQuery Ajaxy + Zend Framework solution mentioned earlier. Plugins for HTML5 History.js + every darn CMS and Framework are coming soon.

The End.

Feel free to get in touch with me.

Benjamin Lupton

Like it. Share it.

Intelligent State Handling: Solutions for horrific hash/hashbang fallbacks and HTML5 History API (pushState) problems http://j.mp/etU7q6

Licensing

Copyright 2011 Benjamin Arthur Lupton Licensed under the Attribution-ShareAlike 3.0 Australia (CC BY-SA 3.0)

Clone this wiki locally