JavaScript Architecture: Backbone.js Routers

02.29.2012

Updated Aug 11, 2012 to reflect current library versions.

In JavaScript Architecture: Backbone.js Views we discussed how to build dynamic apps that change views on the fly using JavaScript. Because view-switching is done without reloading the page or transferring control to a separate page, these are called single-page applications. Single-page applications pose a few issues we need to address:

  • When users hit their browser’s back button, they will be taken away from the app completely rather than back to a previous view within the app itself.
  • Users are only able to link to or bookmark the app itself–not a specific view within the app.
  • Deep views within the app may not be crawlable by search engines.

We want a great experience for our users. Successful apps behave as users would logically expect and users should feel like they can easily navigate back to where they were previously.

Like the topics we’ve addressed before, these issues aren’t specific to Backbone. It’s an issue that naturally arises in any single-page app. Fortunately, Backbone does a great job at addressing it and has a simple API.

Examples

This concept is most easily taught by example so let’s assume we’re building an app that sells shirts. We’ve built it out so we show a grid of shirt images and when the user clicks on a shirt, a panel slides out that covers half of the window and contains more details regarding that specific shirt. Without some extra work, if users are on this extra details view and click the back button in the browser, they will be taken out of the app completely rather than just hiding the extra details view.  This is because, technically, the details view is not a new html page–it’s just another “view” within the same html page.

The browser has no concept of views that are changing within your app and therefore cannot automatically register history steps in this regard. As we discussed in JavaScript Architecture: Backbone.js Views, the definition and granularity of a view can become very blurry. For example, if we pop up a settings panel after a user clicks on a gear icon, does the settings panel merit a history step in the browser so that if the user clicks the back button the panel closes? As much as we would like browsers to figure this out automatically, it’s really a user experience judgement call on our part and we must tell the browser when to register a new history step. This gives us a lot of power.

For starters, let’s take a look at a simple example with Backbone:

<!DOCTYPE html>
<html>
	<head>
		<meta http-equiv="content-type" content="text/html; charset=UTF-8">
		<title>Backbone Example</title>
	</head>
	<body>
		<script src="js/libs/jquery.js"></script>
		<script src="js/libs/underscore.js"></script>
		<script src="js/libs/backbone.js"></script>
		<script type="text/javascript">
			$(function() {
				var AppRouter = Backbone.Router.extend({
					routes: {
						"shirt/id/:id": "showShirt"
					},
 
					showShirt: function(id) {
						alert('Show shirt with id ' + id + '.');
					}
				});
 
				var appRouter = new AppRouter();
				Backbone.history.start();
			});
		</script>
 
		<a href="#shirt/id/5">Shirt with id of 5</a><br>
		<a href="#shirt/id/10">Shirt with id of 10</a><br>
		<a href="#shirt/id/15">Shirt with id of 15</a><br>
	</body>
</html>

You can see we have three links each linking to a shirt with a different id. Notice that the pound sign at the beginning of each link url (e.g., #shirt/id/5) is important as it tells the browser we’re neither moving to a new html page nor refreshing the current page. It’s formally known as a fragment identifier and has been a web standard for quite a while.

In our example, We’ve also set up a backbone router with “routes” which are essentially url patterns that are meaningful to our app. In this case, we have set up a single route, shirt/id/:id, with a handler of showShirt(). The :id portion is called a parameter part. If a user navigates to the url <current_page>/#shirt/id/123123, the fragment would qualify as a match and the id (123123) would be passed as a parameter to the showShirt() function. In our example, if the user clicks the first link we set up, the url will be changed to <current_page>/#shirt/id/5 and Backbone will subsequently call showShirt(5).

To have a little fun, let’s go nuts for donuts and get crazy up in here:

<!DOCTYPE html>
<html>
	<head>
		<meta http-equiv="content-type" content="text/html; charset=UTF-8">
		<title>Backbone Example</title>
	</head>
	<body>
		<script src="js/libs/jquery.js"></script>
		<script src="js/libs/underscore.js"></script>
		<script src="js/libs/backbone.js"></script>
		<script type="text/javascript">
			$(function() {
				var AppRouter = Backbone.Router.extend({
					routes: {
						":product/:attribute/:value": "showProduct"
					},
 
					showProduct: function(product, attribute, value) {
						alert('Show ' + product + ' where ' + attribute + ' = ' + value + '.');
					}
				});
 
				var appRouter = new AppRouter();
				Backbone.history.start();
			});
		</script>
 
		<a href="#shoe/size/12">Size 12 shoes</a><br>
		<a href="#shirt/id/5">Shirt with id of 15</a><br>
		<a href="#hat/color/black">Black hats</a>
	</body> 
</html>

This time we set up our route with three parameter parts. Because there are three parameter parts, three parameters will be passed into showProduct(). Easy enough.

Deep-linking

Now that we have our routes set up, we can deep-link to a specific view within our app. For example, a user might click on the size 12 shoes link which changes the url in their browser to http://code.aaronhardy.com/backbone-router-multi-param/#shirt/id/5. The user may want to share that shirt with a friend so they copy the link and send it in an email. When the user clicks on the link, the app loads, and backbone sees that the url already matches a route. showProduct() will then be automatically called which will immediately show the product details panel to the user. Go ahead, click on the link above and see the concept in action.

Showing requested content

In the above examples we’re just alerting information about the parameter parts. In a real app, the user would expect the app to switch to an appropriate view and load appropriate data. How you handle going from the showProduct() function to switching views has been left up to you. However, one option is, rather than calling a function within the router itself, views can watch the router for route events and update themselves accordingly:

<!DOCTYPE html>
<html>
	<head>
		<meta http-equiv="content-type" content="text/html; charset=UTF-8">
		<title>Backbone Example</title>
	</head>
	<body>
		<script src="js/libs/jquery.js"></script>
		<script src="js/libs/underscore.js"></script>
		<script src="js/libs/backbone.js"></script>
		<script type="text/javascript">
			$(function() {
				var AppRouter = Backbone.Router.extend({
					routes: {
						":product/:attribute/:value": "showProduct"
					}
				});
 
				var appRouter = new AppRouter();
 
				var MyView = Backbone.View.extend({
					initialize: function(options) {
						options.router.on('route:showProduct', function(product, attribute, value) {
							alert('Update to show ' + product + ' where ' + attribute + ' = ' + value + '.');
						});
					}
				});
 
				window.myView = new MyView({router: appRouter});
 
				Backbone.history.start();
			});
		</script>
 
		<a href="#shoe/size/12">Size 12 shoes</a><br>
		<a href="#shirt/id/5">Shirt with id of 5</a><br>
		<a href="#hat/color/black">Black hats</a>
	</body>
</html>

Another option is to share a backbone model amongst the router and views. The router can then set a property on the model and the views can then respond to change events coming from the model.

Fragment identifier vs. pushState

Using fragment identifiers (#) or hashbangs/shebangs (#!) in the manner described above or as used by Twitter (https://twitter.com/#!/Aaronius), Grooveshark (http://grooveshark.com/#!/artist/Gotye/49212), or GMail (https://mail.google.com/mail/ca/#label/isys) doesn’t come without controversy, controversy, and more controversy.

In the end, the answer to the controversy is to use the new HTML5 History (more commonly known as pushState) standard that allows us to change the browser url and manage history steps without using fragment identifiers or forcing a page refresh on the user. The biggest roadblock to this new standard is obtaining browser support, particularly Internet Explorer support. For browsers that don’t support HTML5 History, a fallback must be available which usually entails page refreshes as the user navigates through the app.

Once you’re ready to support pushState, you’ll be glad to know Backbone supports it on an opt-in basis. Read more about how to set up Backbone for pushState.

More Juiciness

Backbone’s router and history have even more options and features so I suggest you check out their respective documentation. Thanks for reading and, as always, feel free to post a comment.

<< JavaScript Architecture: Backbone.js ViewsJavaScript Architecture: RequireJS Dependency Management >>

Tags: , , , , , ,


Comments

03.01.2012 / Brian Rinaldi said:

Good article. Fwiw, hashbang URL’s (#!) have become somewhat controversial lately. For example, see this post – http://danwebb.net/2011/5/28/it-is-about-the-hashbangs. In fact, it is my understanding that Twitter, who popularized this technique, is working hard on trying to remove them in an upcoming update.

03.01.2012 / Aaron Hardy said:

Thanks for bringing this up. It’s definitely controversial. I’ve modified my post quite a bit to bring more attention to this and hopefully point readers in the right direction. I agree with most of the criticisms I’ve read, but FWIW I also agree with one of the commenters on the post you linked to:

The issue I have with this post is it presents no generally viable alternate solution. Right now, and in the forseeable future, the choices are A) use hashbangs to preserve client state, or B) degrade the experience for the users in IE.

Github is one of very few special cases that has an audience that allows it to reasonably choose B. For almost everyone else that lives in a world where upwards of 50% are on IE, that simply is not a reasonable option. IE does not support pushState/replaceState, and doesn’t plan to anytime soon.

If I think a feature improves user experience, I am going to figure out a way to deliver it to as many people as possible. For client-side state changing features, the only real way to do this in the forseeable future is by using hashbangs. We all agree they’re ugly, but they’re simply the lesser of two evils.

Hopefully I’ve satisfactorily clarified these thoughts in my post. Thanks again for commenting and bringing this up.

04.27.2012 / mk said:

How do I .innerHTML in backbone way instead of throwing alert in your final example?

04.30.2012 / Aaron Hardy said:

@mk this.$el.html(…);

07.06.2012 / Shruti said:

Your article was informative.
I want that when i click on a , the contents of the respective div should be appended with the URL.
Can I do it using router?
I have just started with backbone js so not having much idea, please guide me.
Thanks

07.10.2012 / Aaron Hardy said:

Sorry @Shruti, I don’t understand what you’re asking. Can you clarify or post to stackoverflow.com? Thanks.

09.13.2012 / kushal said:

here’s my problem
I have a page in which there are multiple links leading to multiple views in the page …the router routes the page to those views and every click some data is brought via ajax calls into the collection to produced into a view …the problem is that when i click the browser back button instead of re-rendering the view form the created collection it again makes the ajax call ..creates the collection and the renders the view …so is this possible to click back and re-render the previous view without making the ajax call

09.14.2012 / Aaron Hardy said:

Hi Kushal,

Yes, you have full control over what happens when the back button is clicked. You’ve specified a function to be called when a particular route is hit. Something in the function is recreating the collection and view and loading data again. You can just check to see if that stuff has already been done before and, if so, don’t do it again.

10.18.2012 / Daryl B. said:

Any idea what happens if one of your routes requires SSL (https://) and the others do not?
Will pushState be able to do this without completely refreshing the backbone app and all of its asets.


Leave a Comment

Your email address is required but will not be published.




Comment