permalink

109

Building Single Page Applications With jQuery’s Best Friends

Hey guys. Today we’re going to take a look at how you can build a single-page application (SPA) supporting graceful degradation using some of jQuery’s Best friends; DocumentCloud’s Backbone.js, Underscore.js, LAB.js and jQuery templating to name but a few.

Alex Sexton has been giving a great presentation highlighting these tools in few recent conferences and I thought it would be useful to write up a tutorial to compliment their use.

The SPA application we’ll be building today is a three-level bookmarkable image gallery. Before we get started, please feel free to check out the demo below or download the sources for the tutorial.

 

 

Tools Used For Today’s Tutorial

 

What’s A Single-Page Application (SPA)?

 

SPAs are web applications or websites which persistently run in the same page without requiring a reload for further navigation. All of the code needed for your initial load in these applications is driven by either local data or data retrieved from a web server on demand, such as any additional data required from your app that might be driven by user actions.

The basic idea behind an SPA is that regardless of what interactions your users might have with the application, the page doesn’t get reloaded or have it’s control handled by another page outside of the current one.

 

Why Might SPAs Can Be Better Than Multi-Page Applications?

 

SPAs typically contrast from classical multi-page web applications where page changes are regular and the browser is often asked to fetch new content from the server and then reload to fulfil user requests.

The problem with the classical approach is that it has a way of disrupting the user experience quite badly. There’s an obvious transition from one page to the next which requires you to wait until entire new pages have loaded and this often means requesting the same content again and again (eg. the sidebars, header, navigation etc). With an SPA on the other hand, changes of application ‘state’ are handled using approaches such as XHR calls, making the user experience a lot more fluid.

In a server-side web application (without any client-side JavaScript) one can consider the idea of state sequence to be equivalent to the concept of pages. A partial change in an SPA (such as displaying a contact form) implies a change of state in the same way a change of navigation in a server-side app results in a change of page.

 

How Will Today’s App Approach SEO, Hash Routing and Graceful Degradation?

 

In today’s tutorial, we’ll be leveraging the jQuery templating plugin to handle rendering our Backbone views and using hash-based routing for the URLs.

Normally, without any additional work done, this would simply result in a blank page being visible to both search bots (other than Google) and users with JavaScript switched off.

So, how do we handle SEO when bots don’t correctly follow JavaScript-powered routes? How do we also provide an experience for users with JavaScript completely disabled?. In my opinion the solution to hash-bang/based URLs and graceful degradation lie in the same basket.

A degraded experience should still allow users to perform most of the basic content-browsing tasks and the URLs this content exposes through links can be used for your SEO with standard parameter-based queries (ie. JavaScript routing may look like #/books/12345, Degraded URLs can look like /?category=books&bookID=12345).

You can then simply ‘connect’ these two schemas using server-side redirect logic should you need to have a greater level of control over what users are taken to.

In the gallery app, I’ve added in a PHP fallback for bots and NoJS which simply reads in my JSON datastore and provides users with similar album/subalbum/image views (with meta-data). This allows me to be free to use whatever client-side templating solutions I need without worrying about JavaScript being disabled.

 

Backbone.js and Underscore.js

 

In case you haven’t used DocumentCloud’s Backbone.js before, it’s a small open-source library that helps bring a variation of the MVC (Model-View-Controller) architecure pattern to your applications – I say ‘variation’ as Backbone also relies on the idea of Collections in addition to models, views and controllers.

One of the reasons I love Backbone is that it helps give you a structure for your code where you don’t have to tie data so heavily to the DOM. Another useful feature is that when your Backbone models change, the views representing the underlying data can be notified, causing them to re-render.

When writing Backbone apps, extending Backbone.Model allows you to define a model with your own properties and methods whilst the framework makes it easy to get and set attributes, validations and serialize the object. Backbone proxies to the Underscore library for handling it’s collections of models.

So..what’s Underscore? Well, it’s basically a utility-belt library that provides a lot of the functional support that you would expect in something like Ruby, but without extending any of the built-in JavaScript objects. (Some call it the tie that goes along with jQuery’s tux), but really, it just gives you some useful functions you might normally be adding to your own utils.js file anyway like each, map, isEmpty, isElement and more.

Going back to Backbone, views can be created by extending Backbone.view and passing in options, models, collections etc. You can also specify a render function on the view to set it up with templating (in today’s tutorial we’ll be using the jQuery templating plugin for this).

I’ll be going over Backbone in more detail shortly.

 

Client-Side Templating With The jQuery Templating Plugin

 

As mentioned, we’re also going to be using the jQuery templating plugin to render our Backbone views today. It basically allows you to create simple client-side templates that are easily re-usable where templates are written in HTML with template tags whilst jQuery code is used for performing the actual data population. A very simple example of how to use the plugin can be found below:

 var albums = [
 { Title: "My Vacation In Malibu", AlbumYear: "1993" },
 { Title: "A Trip To The Sea-side", AlbumYear: "1992" }
 ];
 //define the markup for our template
 var template_markup = "
  • ${Title} (${AlbumYear})
"; //compile our markup above as a template called //'albumTemplate' $.template( "albumTemplate", template_markup ); //render the template using 'albums' as our data //source then insert the HTML thats rendered under //the albumList element $.tmpl( "albumTemplate", albums ) .appendTo( "#albumList" );
 

    The official documentation on the jQuery templating plugin is an excellent place to find more examples if you haven’t used it before.

    When it comes to getting templates working with Backbone, this process is very straight-forward. All we need to do is add our template-handling to a view’s render() function based on a template defined in our app’s markup and then append it to our SPA’s main container as needed. This is a little different to how one might use the templating plugin on it’s own, but as you’ll see in the tutorial we’re still going to using $.tmpl for it’s rendering capabilities.

     

    LAB.js Asynchronous Script Loading

     

    Kyle Simpson’s LABjs (Loading And Blocking JavaScript) is a great general purpose script loader which allows you to load scripts in parallel as the browser allows. It also maintains the execution order required when you express the need for keeping dependencies safe, which can be done by inserting .wait() in your chain. In the below example, we wait for jQuery to execute before loading any additional dependancies or plugins and finally wait for those to execute before loading our main application.

     $LAB
     .script("jquery.js").wait()
     .script("another.dependency.js")
     .script("relies.onabove.dependency.js").wait()
     .script("app.js");
    

    There are a lot of great script loaders out there at the moment (including RequireJS) but because the code for today’s app doesn’t really require modules (and LAB.js has a very small footprint) it made sense to select it for our use-case. You can take a look at some more LAB.js examples over at it’s main documentation page.

     
     

    Today’s Single-Page MVC Gallery Application

    Introduction

    In today’s tutorial we’re going to build a three-level, fully bookmarkable image gallery that supports custom views for albums, subalbums and individual photos.

    Backbone will be used to provide a taste of MVC architecture to the app, Underscore for it’s utility functions, LAB.js for asynchronous script loading and the jQuery templating plugin will be used to render our client-side views.

    Data caching is something that can often be ignored in tutorials about client-side app development, so I’m also going to introduce you to Dustin Diaz (Twitter)’s CacheProvider which allows you to cache data both in memory and using localStorage if it’s available.

    As I also covered, we’ll be including some graceful degradation in today’s application as well so that even if JavaScript is turned off, you should still be able to browse the photos in their normal hierarchy.

     

    Application Structure

    Diving right in, let’s first define the structure of our application. The idea behind using Backbone is that it allows you organize your interface into logical views, backed by models, each of which can be updated independently when the model changes, without having to redraw the page.

    Models defined using Backbone.Model contain the interactive data and logic around the information being used in our application. For the gallery app, we’re going to define a simple model called ‘Photo’ to represent the data around image entries.

    Backbone.Collection allows you to define ordered sets of models which can bind ‘change’ events to be notified when models in a collection are modified. We’re going to create a collection called ‘PhotoCollection’ to represent collections of ‘Photo’s.

    Our application views can be defined through Backbone.View. Views don’t actually have anything to do with HTML, but they’re more a convenience concept that support a number of client-side templating solutions.

    Views save you having to dig through JSON, do DOM lookups and update your HTML manually by binding a view’s render function to a model’s ‘change’ event – this way anywhere a model’s data is shown in your UI, it’s always up to date. In our app we’ll be creating an IndexView, SubalbumView and PhotoView to reprepsent the three possible gallery levels that can be viewed.

    The final piece of the puzzle is something to handle our application routes for client-side URL fragments. We also need something that can correctly connect these to both actions and events. Backbone.Controller helps us achieve this and for our gallery app we’re simply going to define a Gallery controller for our routing.

    Data Structure

    The JSON data-structure behind our application needs to be able to support the concept of albums, sub-albums and photos. In addition, we’ll be adding the requirement that each album needs to have it’s own folder thumbnail and each photo in a sub-album needs to have it’s own thumbnail as well. We’ll also need the flexibility to include as much (or as little) meta-data around our images as we need.

    [
      //this is an album with additional meta-data
      {
        "title": "Album title",
        "artist": "Album artists",
        "image": "images/1.jpg",
        "years": "Album date/year",
        "pid" : 0,
        "url": "additional meta-data",
       //this is a sub-album with it's own data
    	"subalbum": [
                     //this is a photo in a sub-album
    				  {
    					"title": "photo title",
    					"artist": "photo artist",
    					"image": "images/michael_small.jpg",
    					"large_image": "images/michael_large.jpg",
    					"price": 13,
    					"pid": 0,
    					"url": "additional url meta-data"
    				  }
    				]
    	}
    ]
    

    jQuery Templating Structure

     

    Application Index (Album view)

    <script id="indexTmpl" type="text/x-jquery-tmpl">
    
    ${attributes.artist}
    ${attributes.title}
    ${attributes.years}
    </script>

     

    Subalbum View

    <script id="subindexTmpl" type="text/x-jquery-tmpl">
    
    ${attributes.artist}
    ${attributes.title}
    $${attributes.price}
    </script>
     

    Photo View

    <script id="itemTmpl" type="text/x-jquery-tmpl">
    
    ${attributes.title}
    ${attributes.artist}
    ${attributes.title}
    $${attributes.price}

    </script>

    Models, Views, Controllers & Collections

     

    Here we create the model ‘Photo’ ; used to define individual image items. ‘subalbum’ returns a reference to the current subalbum being viewed via the gallery for use when accessing a Photo item through a hash URL We also define a new CacheProvider for use in our Controller later.

    var cache = new CacheProvider;
    var Photo = Backbone.Model.extend({
       subalbum: function() { return 'c' + gallery._currentsub; }
    });
    

    PhotoCollection: A collection of Photo items used in index, subalbum and photo views.

    var PhotoCollection = Backbone.Collection.extend({
        model: Photo,
        comparator: function(item) {
            return item.get('pid');
        }
    });
    

     

    IndexView: The default view seen when opening up the application for the first time. This contains the first level of images in the JSON store (the level-one albums). Prior to rendering our jQuery templates here we remove any messages or elements displayed in the version where JavaScript is disabled.

    var IndexView = Backbone.View.extend({
        el: $('#main'),
        indexTemplate: $("#indexTmpl").template(),
        render: function() {
    		//remove any graceful degradation views/notices
            $('.jstest,'.gallery').remove();
            var sg = this;
            this.el.fadeOut('fast', function() {
                sg.el.empty();
                $.tmpl(sg.indexTemplate, sg.model.toArray())
    .appendTo(sg.el);
                sg.el.fadeIn('fast');
            });
            return this;
        }
    });
    

    SubalbumView: The view reached when clicking on a level-one album or browsing to a subalbum bookmark. This contains the images found in the ‘subalbum’ section of an album entry. Clicking on any of the images shown in a subalbum takes you to the PhotoView of that specific image.

    var SubalbumView = Backbone.View.extend({
        el: $('#main'),
        indexTemplate: $("#subindexTmpl").template(),
           initialize: function(options){
        },
        render: function() {
            var sg = this;
            this.el.fadeOut('fast', function() {
                sg.el.empty();
                $.tmpl(sg.indexTemplate,
                sg.model.toArray())
    .appendTo(sg.el);
                sg.el.fadeIn('fast');
            });
            return this;
        }
    });
    

     

    PhotoView: The single-photo view for a single image on the third-level of the application. This is reached either by clicking on an image at the second/subalbum level or browsing to a bookmarked photo in a subalbum.

    var PhotoView = Backbone.View.extend({
        el: $('#main'),
        itemTemplate: $("#itemTmpl").template(),
        initialize: function(options) {
            this.album = options.album;
        },
        render: function() {
            var sg = this;
            this.el.fadeOut('fast', function() {
                sg.el.empty();
                $.tmpl(sg.itemTemplate, sg.model)
                .appendTo(sg.el);
                sg.el.fadeIn('fast');
            });
            return this;
        }
    });
    

    Gallery: The controller that defines our main application ‘gallery’. Here we handle how routes should be interpreted, the basic initialization of the application with data through an $.ajax call to fetch our JSON store and the creation of collections and views based on the models defined previously.

    var Gallery = Backbone.Controller.extend({
        _index: null,
        _photos: null,
        _album :null,
    	_subalbums:null,
    	_subphotos:null,
    	_data:null,
    	_photosview:null,
    	_currentsub:null,
        routes: {
            "": "index",
            "subalbum/:id": "subindex",
            "subalbum/:id/" : "directphoto",
            "subalbum/:id/:num" : "hashphoto"
        },
        initialize: function(options) {
            var ws = this;
            if (this._index === null){
                $.ajax({
                    url: 'data/album1.json',
                    dataType: 'json',
                    data: {},
                    success: function(data) {
    				    ws._data = data;
                        ws._photos =
                        new PhotoCollection(data);
                        ws._index =
                        new IndexView({model: ws._photos});
                        Backbone.history.loadUrl();
                    }
                });
                return this;
            }
            return this;
        },
    	//Handle rendering the initial view for the
    	//application
        index: function() {
            this._index.render();
        },
    

     

    Gallery -> hashsub: Handle URL routing for subalbums. As subalbums aren’t traversed in the default initialization of the app, here we create a new PhotoCollection for a particular subalbum based on indices passed through the UI. We then create a new SubalbumView instance, render the subalbums and set the current subphotos array to contain our subalbum Photo items. All of this is cached using the CacheProvider we defined earlier.

    hashsub:function(id){
    	   var properindex = id.replace('c','');
    	   this._currentsub = properindex;
    	   this._subphotos = cache.get('pc' + properindex)
    || cache.set('pc' + properindex, new PhotoCollection(
    this._data[properindex].subalbum));
    	   this._subalbums = cache.get('sv' + properindex)
    || cache.set('sv' + properindex, new SubalbumView(
    {model: this._subphotos}));
    	   this._subalbums.render();
    	},
    

     

    Gallery -> hashphoto: Handle routing for access to specific images within subalbums. This method checks to see if an existing subphotos object exists (ie. if we’ve already visited the subalbum before). If it doesn’t, we generate a new PhotoCollection and finally create a new PhotoView to display the image that was being queried for. As per hashsub, variable/data caching is employed here too.

    hashphoto: function(num, id){
    	    this._currentsub = num;
    	    num = num.replace('c','');
    		if(this._subphotos == undefined){
    		   this._subphotos = cache.get('pc' + num) ||
    cache.set('pc' + num, new PhotoCollection(
    this._data[num].subalbum));
    		 }
    	    this._subphotos.at(id)._view =
    new PhotoView({model: this._subphotos.at(id)});
    	    this._subphotos.at(id)._view.render();
    	  }
    

    Script Loading & Dependency Management

     

    How you structure your LABS.js chains can vary, but as a rule of thumb I like to order mine in a dependency hierarchy. Below is a loose version of this where we wait until jQuery has loaded before loading any additional scripts. We also wait on the last gallery.js dependency to finish loading before we load and execute our application (as per the LAB.js example earlier).

    		   $LAB
    		   .script("jquery.min.js").wait()
    		   .script("jquery.tmpl.min.js")
    		   .script("underscore-min.js")
    		   .script("backbone-min.js")
    		   .script("cacheprovider.js").wait()
    		   .script("gallery.js");
    
     

    Graceful Degradation For Users With JavaScript Disabled

     

    When JavaScript is either unavailable or a bot’s attempting to crawl our app, we need to be able to provide a basic user-experience that still provides access to the content. In order to do this, I wrote a short PHP script which uses the same JSON gallery store as it’s data source. A snippet of this script can be seen below:

    //expose convenient access to subalbums
    foreach ($json_a as $p => $k){
        foreach($k["subalbum"] as $sub){
    		 $subalbums[$i][$j] = $sub;
             $j++;
    	}
    	$i++;
    }
    //handle 'view' switching
    switch($folderType){
    	case "subalbum":
    		echo "";
    	break;
    	default:
    	    $ind = 0;
    		echo "";
    	break;
    }
    

    JSDoc Documentation

     

    Documentation tools are in no way specific to SPAs, but it’s always a good idea to document your code and provide your end-users with a readable form of documentation. JSDoc (The JSDoc Toolkit) is a useful tool for automatically generating multi-page HTML, XML or JSON documentation from comments in your JavaScript code.

    It’s very straight-forward to use and can be run from the command-line or via a shell script once installed. I’ve commented gallery.js using JSDoc compatible syntax and you can see an example of how to format your comments for it below:

       /**
       @description Get's an item based on it's ID
       @param {Integer} itemId
       @type Array
       @returns {returnType} returnDescription
       @author <a href="mailto:test@gmail.com">Test Author</a>
       */
       function getItem(itemId) {
       }

    If you’re interested in reading more about JSDoc, you can check out the official project homepage or the latest documentation.

    SPA Application Building Tips

     

    If you’re looking to begin building your own SPA with jQuery’s Best Friends, I highly recommend planning out your application structure in advance. For example, knowing the different ‘views’ you wish to offer users make writing both your data-structures, template code and finally your Backbone models and views significantly more straight-forward to write.

    Altering your app at a later stage in the coding process isn’t difficult, but it can be time-consuming redefining data-structures, routing and templates then getting them to work in harmony again. That said, *as long* as you have some idea of your app structure in advance, you should be okay. I’ve always found SPAs quite fun to code and I’m sure if you haven’t tried them yet, you will too.

    Downloads & Source Code

     

    To check out a demo of today’s application, feel free to click on Demo. The sources for the tutorial can be found on GitHub and clicking ‘download’ will get you the latest build.

     

     

    Conclusions

     

    We all know that jQuery makes it easy to write scripts and applications that interact with the DOM, but jQuery’s best friends give you the power to take that further, adding the ability to add structure to your application architecture, provide you with utilities so you don’t have to code them yourself and other benefits like client-side templating and lightweight scriptloading.

    There’s absolutely no requirement that you have to use all of these tools together but I hope this post encourages you to try using them in your apps – they really can help you build better cleaner solutions. For more reading on MVC or building large scale applications with jQuery, feel free to check out my post on the topic.

    And that’s it!. If you’ve found today’s tutorial helpful, please feel free to share it with your friends and colleagues by clicking on the Building Single Page Applications With jQuery’s Best Friends button below.

    Thanks and good luck with your projects!

     

    Update Feb 23 , 2001

    Rafael in the comments noticed that there was an issue where directly linking to the photo view without caching was causing the progressive enhancement fallbacks to render rather than being removed. This issue has been fixed on both github and the live demo so if you grab any of the sources from this page they will be up-to-date.

    References

     

    [1] The Single-Page Application Manifesto http://itsnat.sourceforge.net/php/spim/spi_manifesto_en.php

    [2] Building Apps With Backbone.js http://www.oneofthesedaysblog.com/backbone-js/

    [3] The Backbone.js Store (recommended reading. my initial inspiration for writing about Backbone further) http://www.elfsternberg.com/2010/12/08/backbonejs-introducing-backbone-store/

    [4] Script loading with Lab.js and Require.js http://msdn.microsoft.com/en-us/scriptjunkie/ff943568

     

    109 Comments

    1. Thanks so much for for the kind words. Your tutorial is excellent, by the way, but I've since updated The Backbone Store to version 0.2, and version 0.3 is coming to show both data aggregation (get rid of all those _variable things!) and usinge jQuery 1.5 Deferreds and Promises to make animation smooth and predictable.

      If your views have a lot in common (if they share the same viewport, for example), then it makes sense to abstract out their common code into a superclass and extend that. Backbone makes it super-easy because extend() is inherited.

      • Hey Elf. Thanks for writing up the original Backbone store. I think a lot of people found it useful and I'm honored you liked my tutorial.

        I definitely look forward to checking out the 0.3 version of the store – I've been playing around with promises and deferreds myself lately but haven't had a chance to see how they would benefit being used with Backbone. The tips are very welcome!

    2. Pingback: JQuery Resources « WindyGallery’s Weblog

    3. Pingback: JavaScript Magazine Blog for JSMag » Blog Archive » News roundup: Delayed script execution, Jdrop, $script.js, Amplify

    4. Pingback: Single Page Applications with jQuery « Apps « Technology Demos & Downloads

    5. hi.. nice article. I like the way you explain the steps involved. Btw, there is a js error when i open the demo in IE. tried to fix it but cudn't. any ideas why it cud be coming and how to fix it. I am a total newbie at this stuff..
      thanks

        • Hi Addy, thanks for responding.. I am experiencing this in IE 7 and 8, haven't tried in 6 though. the javascript error says "this._index is null or not an object".

          • Hi, I had the same error, too.

            The thing is "$.ajax()" is asynchronous by default and while it executes, there is a call to "Gallery.index()" method caused by Url resolving, while"Gallery._index" is still "null" and will be set after getting success response from server (in "success()" callback) .

            Change "Gallery.index()" method to be:

            index: function () {
            if (this._index) {
            this._index.render();
            }
            }

            Here "if" checks is "Gallery._index" is set and only then calls "render" method.

            Another solution is to add "async: false" in "$ajax()" call.

            I can't see how it works in other browsers :) Or we just don't see this error there, because it is not popped up as in IE.

            Hope it helped.

    6. Without any previous caching, with JS on, when going directly to some sub-album, say
      http://addyosmani.com/resources/backbonegallery/i
      then it displays the album _plus_ all those non-JS index page items (top level collections). When clicking the link on one of those, say
      http://addyosmani.com/resources/backbonegallery/i… for 'Modern Pop/R&B 1999-2011'
      it displays an index page again (with collection covers styled as JS-turned-on version normally displays them, this time).

    7. Pingback: How to Build Single Page Applications with jQuery

    8. Hi Addy, thanks for responding.. I am experiencing this in IE 7 and 8, haven't tried in 6 though. the javascript error says "this._index is null or not an object".

    9. @Ashley
      The null pointer occurs when the browser tries to render the page before the data is ready. You can prevent this by happening by making the ajax call synchronous. To do this, insert the following line after line 150 in gallery.js:

      async: false,

    10. Pingback: How to Build Single Page Applications with jQuery | Appwebmaster

    11. Pingback: Delicious Bookmarks for March 27th from 03:10 to 03:26 « Lâmôlabs

    12. Pingback: Building JavaScript Web Apps With MVC & Spine.js

    13. Pingback: 开源中最好的Web开发的资源 | 酷壳 - CoolShell.cn

    14. Pingback: 九王爷的府邸 » Blog Archive » Web开发学习的资源

    15. Pingback: 开源中最好的Web开发的资源 | Elvin Lee 's Blog

    16. Pingback: 开源中最好的Web开发的资源 | UECSS.COM

    17. Pingback: 开源中最好的Web开发的资源 – WEB前端开发- 专注前端开发,关注用户体验

    18. Pingback: 开源中最好的Web开发的资源 | Q1前端开发

    19. Pingback: 开源中最好的Web开发的资源 | IT无敌

    20. Pingback: 开源中最好的Web开发的资源 « ARM9 & Embedded System

    21. Pingback: 开源中最好的Web开发的资源汇总 | 百锐网

    22. Pingback: 开源中最好的 Web 开发的资源 at 乱炖

    23. Pingback: 开源中最好的 Web 开发的资源 - 探索

    24. Pingback: 开源中最好的Web开发的资源 [zz] | 拈花微笑

    25. Pingback: Page to Page | Blog | 开源中最好的Web开发的资源

    26. Pingback: 开源中最好的 Web 开发的资源(下) | IMDevice

    27. Pingback: 开源中最好的web开发工具 | Foodcoming食尚点滴|宅食客

    28. Pingback: 开源中最好的Web开发资源汇总 | 博客水木

    29. Pingback: [转]开源中最好的Web开发的资源 « NeverBest!我还能做的更好 – 007boy | im007boy

    30. Pingback: learning web | P.H.CN

    Leave a Reply

    Required fields are marked *.