I Coded the Angular Tutorial App in Backbone and it Took 260% More Code

tl;dr

The people at AngularJS created their PhoneCat tutorial app with 48 lines of JavaScript . When we coded the same app using Backbone instead of Angular, we found it took 171 lines of JavaScript – 260% more code. Here, we present a step-by-step tutorial of how we replicated the Angular tutorial in Backbone.

A while ago I decided to check out Angular since everyone’s been talking about it. I went to the Angular website and worked through their fantastic tutorial. I could see the merits of their approach, but I was left with the nagging feeling that if I coded the same application in Backbone it would only take me a few extra lines of code – a binding here and a callback there and I’d be done, right?

It turns out that I was wrong. After actually reproducing the Angular tutorial in Backbone, I found that it took significantly more JavaScript as well as a good bit of finesse. I thought I would share my Backbone version of the Angular tutorial as a step-by-step guide so that it is easy to make a direct comparison between Angular and Backbone.

To get started, clone the backbone-phonecat repository on GitHub into your current directory.

git clone https://github.com/204NoContent/backbone-phonecat.git

We’re using Node as our back-end so make sure it is installed.

node --version

If Node isn’t there, download and install it.

In order to start up the web server that powers this tutorial, cd into the backbone-phonecat directory and run

node app.js

Node should output Express server listening on port 8888, at which point we can navigate over to localhost:8888, to see a list of phones displayed on the screen.
Screen Shot 2014-04-09 at 4.49.43 PM.png

What’s on the screen presently is the finished product of what we will be building in this tutorial. The tutorial is structured so that we can directly jump to any step along the way by checking out a previous commit using git. In all, there are 11 steps, step-0 through step-10. We’ll begin by resetting our code to step-0, but since we don’t necessarily want to take down our web server by killing the running Node process, it is best to leave the Node process running in the current terminal and open up a new terminal window or tab. Once again cd into the bacbone-phonecat directory.

Step-0 Bootstrapping the Backbone App #

To reset our work-space to the first step run

git checkout -f step-0

Refresh the page at localhost:8888 and text Nothing here yet! should be the only thing on the screen. Node is currently generating this output by serving up the following file

views/index.ejs

<!DOCTYPE html>
<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <title><%= title %></title>
        <link rel="stylesheet" href="/stylesheets/app.css">
        <link rel="stylesheet" href="/stylesheets/bootstrap.css">

        <!-- These scripts would be a single cacheable file in production -->
        <script src="/javascripts/lib/jquery.js"></script>
        <script src="/javascripts/lib/underscore.js"></script>
        <script src="/javascripts/lib/backbone.js"></script>
        <script src="/javascripts/jst.js"></script>

        <script src="/javascripts/router.js"></script>
        <script src="/javascripts/init.js"></script>
    </head>
    <body>
        <section id='main'>
            Nothing here yet!
        </section>
    </body>
</html>

The text Nothing here yet! appears on the screen because it is the content of our main section. This static text is included on the server side for this step only. All future steps will use Backbone to dynamically generate all page content.

Since we’re just tying to bootstrap the app, Backbone does not have a large role so far. All we’re doing is loading Backbone’s requirements, then Backbone itself. After that we load jst.js, which is an Underscore.js templating dictionary that our Node server will automatically generate for us (which means you should ignore the specifics of the code in that file). The last two scripts are the only real parts of our Backbone app so far.

public/javascripts/router.js

Router = Backbone.Router.extend({

    routes: {
    }
});

router.js is a place for us to define Backbone routes in the future. Currently, no URLs will match since the routes object is empty.

public/javascripts/init.js

App = new Router();

$(document).ready(function () {
    Backbone.history.start({ pushState: true });
});

init.js creates new instance of the Backbone router and then tells Backbone to start monitoring browser history changes (page navigation).

Step-1 Static Phone List #

Reset the work-space to the next step by running

git checkout -f step-1

We’ll discuss the more important changes below, and you can see the full diff on github, or by running git diff step-0 step-1

The objective of this section is to use Backbone to fill in the contents of the main section by generating some static HTML . To do this we’re going to need to create a route that will match the root path of our application http://localhost:8888.

public/javascripts/router.js

Router = Backbone.Router.extend({

    routes: {
        '': 'phonesIndex',
    },

    phonesIndex: function () {
        new PhonesIndexView({ el: 'section#main' });
    }
});

Here we match our application’s root path '' to the phonesIndex method of the router that we also defined. The phonesIndex method instantiates a new Backbone view and tells it to place its content inside of <section id='main'>, which by the way, is no longer populated with any placeholder text from the server.

Next, we need a
need to create a Backbone view.

public/javascripts/views/phones/index_view.js

PhonesIndexView = Backbone.View.extend({

    initialize: function () {
        this.render();
    },

    render: function () {
        this.$el.html(JST['phones/index']());
    }
});

All this view does is render the content of a JavaScript template file public/javascripts/templates/phones/index.jst, which we indicate by passing phones/index as the look up key to our JST dictionary.

public/javascripts/templates/phones/index.jst

<ul>
    <li>
        <span>Nexus S</span>
        <p>
            Fast just got faster with Nexus S.
        </p>
    </li>
    <li>
        <span>Motorola XOOM™ with Wi-Fi</span>
        <p>
            The Next, Next Generation tablet.
        </p>
    </li>
</ul>

That’s pretty much it for this section, the only other change was to make sure we told our browser about the newly created JavaScript files. So in views/index.ejs , we added <script src="/javascripts/views/phones/index_view.js"></script>, but that isn’t very interesting and we won’t be mentioning script loading changes again.

Navigating over to localhost:8888 now shows the result of the Backbone generated static HTML.

Screen Shot 2014-04-09 at 6.14.39 PM.png

Step-2 Backbone Models and Collections #

Reset the work-space to the next step by running

git checkout -f step-2

The full diff can be found on github.

The aim of this section is to use Backbone to dynamically create a list of phones from some source data. To do this we’ll make a Backbone collection of Backbone models, and then pass each of those models to a template to be rendered out as HTML. First let’s create the phone model.

public/javascripts/models/phone.js

Phone = Backbone.Model;

Super simple, but now we can create new instances of Phone and we’ll have access to all the goodness that Backbone provides for models, like being members of Backbone collections and seamless integration into the event bus.

Next up, the Backbone collection to house Phone models

public/javascripts/collections/phones_collection.js

PhonesCollection = Backbone.Collection.extend({
    model: Phone
});

We tell Backbone that our PhonesCollection will be made up of phone models by setting the value of model to Phone. When a new collection is created, Backbone will automatically convert any initialization data to their equivalent representation as Phone models.

We’ll create a new phones collection with some hard coded data in our phones/index_view.js.

public/javascripts/views/phones/index_view.js

PhonesIndexView = Backbone.View.extend({

    initialize: function () {
        this.collection = new PhonesCollection([
            {'name': 'Nexus S',
             'snippet': 'Fast just got faster with Nexus S.'},
            {'name': 'Motorola XOOM™ with Wi-Fi',
             'snippet': 'The Next, Next Generation tablet.'},
            {'name': 'MOTOROLA XOOM™',
             'snippet': 'The Next, Next Generation tablet.'}
        ]);

        this.render();

        new PhonesIndexListView({
            el: this.$('ul.phones'),
            collection: this.collection
        });
    },

    render: function () {
        this.$el.html(JST['phones/index']());
    }
});

this.collection is a Backbone collection of Backbone Phone models, and all we had to do is input the data. We’ve also created an instance of a Backbone view called PhonesIndexListView, which we’ve yet to create. We’ve altered the template of phones/index to only contain a single line <ul class='phones'></ul> so that we have a place for PhonesIndexListView to render its content.

The job of PhonesIndexListView will be to iterate over every phone in the phones collection and render out the corresponding phone template. Let’s create it now.

public/javascripts/views/phones/index_list_view.js

PhonesIndexListView = Backbone.View.extend({

    initialize: function () {
        this.render();
    },

    render: function () {
        this.$el.html('');
        this.collection.each(this.renderPhone, this);
    },

    renderPhone: function (phone) {
        this.$el.append(new PhonesIndexListPhoneView({
            tagName: 'li',
            model: phone
        }).el);
    }
});

Currently, on initialization, the view calls its own render method that wipes the HTML of its assigned <ul>. Then it passes each model in the phones collection to renderPhone. The job of renderPhone is to pass each phone model to a new view that will process the model into HTML. That processed HTML will then be accessible through PhonesIndexListPhoneView‘s el method. That rendered HTML for each model view will then be appended to the phones lists <ul>. The end result is the HTML of all the phones will show up on a list.

PhonesIndexListPhoneView is quite simple since all it does is convert a phone model to its HTML equivalent.

public/javascripts/views/phones/index_list_phone_view.js

PhonesIndexListPhoneView = Backbone.View.extend({

    initialize: function () {
        this.render();
    },

    render: function () {
        this.$el.html(JST['phones/index_list_phone'](this.model));
    }
});

The only interesting bit is that we are passing the phone model to our JavaScript template by referencing it as the argument to the JST template method. The properties (methods) of the phone model are automatically placed in the local scope of the template.

public/javascripts/templates/phones/index_list_phone.jst

[[= get('name') ]]
<p>[[= get('snippet') ]]</p>

Here we are using the notation [[ for embedded JavaScript, where the inclusion of the equals sign [[= indicates that the the evaluated expression will be interpolated to a string. The Backbone model method get is a convenience method for getting the value of one of the phone’s attributes, and is the complement of the Backbone’s set method (which should almost always be used to set all model attributes so that all model changes will be picked up by event bus).

Details aside, the name and snippet of each phone will now be dynamically written to the screen. We can see this in action by navigating over to localhost:8888.
Screen Shot 2014-04-09 at 7.32.57 PM.png

Step-3 Interactive Search #

Reset the work-space to the next step by running

git checkout -f step-3

The full diff can be found on github.

We want to add a search field to the page so that we can display only those phones that match the search criteria. To make this happen, we’ll give our phones collection the ability to filter itself in a similar fashion to Angular’s query filter. We can create this new method by customizing the out-of-the-box filter collection method that Backbone (Underscore) provides.

public/javascripts/collections/phones_collection.js

PhonesCollection = Backbone.Collection.extend({
    model: Phone,

    query: function (query) {
        if (!query || query === '') return this.models;
        return this.filter(function (phone) {
            return phone.values().join().match(new RegExp(query, 'i'));
        });
    }
});

Now when we call call query, on our PhonesCollection we will get back an array of Phone models that match our search term.

Before we go any farther, let’s take a moment to think about the specifics of what we want to happen. We would like there to be an input field on the screen where the user can enter a search term. The list of phones should be updated with each keystroke to reflect the current state of the input field. This means that we are going to need the phones collection to update and render itself every time a user types a character in the input field. There are many techniques that can be used to accomplish this behavior, but the technique we are going to use here is to create a Backbone model called Filter to collect user input.

public/javascripts/models/filter.js

Filter = Backbone.Model;

The advantage of using a Backbone model is that Backbone models emit change events whenever any of their attributes change. This sort of behavior is exactly what we need. If we can link the state of our search input field to the state of Filter, then whenever the input field is changed, our Filter model will emit those change events. We’ll listen to Filter change events and update our collection accordingly. First lets alter the index view to create a new instance of our Filter and its corresponding view

NOTE: vertical ellipses mean that code has been omitted for brevity.

PhonesIndexView = Backbone.View.extend({

    initialize: function () {
        this.filter = new Filter();
.
.
.
        new PhonesFilterView({
            el: this.$('.filter'),
            model: this.filter,
            collection: this.collection
        });

        new PhonesIndexListView({
            el: this.$('ul.phones'),
            model: this.filter,
            collection: this.collection
        });
    },
.
.
.

We created a filter instance and passed it down to both a new PhonesFilterView and PhonesIndexListView. Creating the filter model in this view and passing it to both child views means that each view will have access to filter model events.

First, let’s examine the PhonesFilterView

public/javascripts/views/phones/filter_view.js


PhonesFilterView = Backbone.View.extend({

    events: {
        'keydown input.query': 'setQuery',
    },

    initialize: function () {
        this.render();
    },

    render: function () {
        this.$el.html(JST['phones/filter']());
    },

    setQuery: function (event) {
        // make it snappy using keydown and pushing it to next tick
        window.setTimeout($.proxy(function() {
            this.model.set('query', event.target.value.replace(/^\s+|\s+$/g, ''));
        }, this), 0);
    }
});

This view renders out an input tag in its template, but more importantly it uses Backbone’s event hash to set up a mapping between input keydown events and its own setQuery method. setQuery looks a little funky but all it does is set the query attribute of our filter model to the current value of the input field. When an attribute of the filter model is set, Backbone will check if the value has changed and if so, it will trigger a 'change:attr' event on the model, where attr is the name of the attribute that was changed. So in our case, whenever the query value of the filter changes, Backbone will trigger 'change:query' on the filter model.

We can utilize this behavior to update our phone collection by listening for those change events.

public/javascripts/views/phones/index_list_view.js

PhonesIndexListView = Backbone.View.extend({

    initialize: function () {
        this.filtered_collection = new PhonesCollection();
        this.listenTo(this.filtered_collection, 'add', this.renderPhone);

        this.render();

        this.listenTo(this.model, 'change:query', this.render);
    },

    render: function () {
        var filtered_phones = this.collection.query(this.model.get('query'));
        this.filtered_collection.set(filtered_phones);
    },

    renderPhone: function (phone) {
        var position = this.filtered_collection.indexOf(phone);
        this.phoneView = new PhonesIndexListPhoneView({
            tagName: 'li',
            model: phone
        });

        if (position === 0) {
            this.$el.prepend(this.phoneView.el);
        } else {
            $(this.$('li')[position - 1]).after(this.phoneView.el);
        }
    }
});

There is bit going on here, so we’ll take it one step at a time. Toward the end of the initialization method, we set up a listener for filter changes. When a change is detected, the render method is called which filters the collection based on the query. The filtered collection is then set with the models that made it through the query filter. A collection set in Backbone does a smart update of the collection, adding and removing models as necessary. In addition, Backbone triggers an add or a remove event for every model that was added or removed from the collection. In the first part of our initialization, we set up a listener to render any models that are added to the filtered collection. The result is that each model added to the collection appears on the screen (the fancy bit in the render method just ensures that each model appears in the correct order). The only thing left to do is to remove a phone from the screen when it is removed from a collection. This is accomplished in the phone model view.

public/javascripts/views/phones/index_list_phone_view.js

PhonesIndexListPhoneView = Backbone.View.extend({

    initialize: function () {
        this.listenTo(this.model, 'remove', this.remove);
        this.render();
    },
.
.
.

Here we listen to the phone remove event and use Backbone’s built in View remove method to detach listeners and remove the element from the screen.

Navigating over to localhost:8888 now displays a list of phones that can be filtered with the search field.
Screen Shot 2014-04-09 at 8.55.53 PM.png

Step-4 Phone Ordering #

Reset the work-space to the next step by running

git checkout -f step-4

The full diff can be found on github.

The idea behind ordering is very similar to text search. We’re going to have an order drop down of possible ordering values. When one is selected, a corresponding Filter sort attribute will be updated to reflect the current state. The Filter will naturally emit a change event, which in this case will be 'change:sort'. We can listen for this event in the phones index list view and re-render the collection. We also want to default the sorting to display the newest phone first. To do that, we default the Filter model, since Backbone supports a default hash out-of-the-box.

public/javascripts/models/filter.js

Filter = Backbone.Model.extend({
    defaults: {
        sortBy: 'age'
    }
});

Next we alter the filter view to listen for drop down change events and respond accordingly

public/javascripts/views/phones/filter_view.js

PhonesFilterView = Backbone.View.extend({

    events: {
        'keydown input.query': 'setQuery',
        'change select.sort': 'setSort'
    },
.
.
.
    render: function () {
        // these next few lines massage data to make
        // the template more straightforward
        this.order_options = [{ value: 'name', text: 'Alphabetical'}, { value: 'age', text: 'Newest' }];
        var selected_option = _.findWhere(this.order_options, { value: this.model.get('sortBy') }, this);
        if (selected_option) selected_option.selected = true;

        this.$el.html(JST['phones/filter']({ order_options: this.order_options }));
    },
.
.
.
    setSort: function (event) {
        this.model.set('sortBy', event.target.value);
    }
});

The template reference here is somewhat interesting because we iterate over an array of drop down values and default the selection to the drop down value where the selected attribute is set to true.

public/javascripts/templates/phones/filter.jst

Search: 
<input class='query'>
Sort by: 
<select class='sort'>
    [[ _.each(order_options, function (option) { ]]
        <option value="[[= option.value ]]" [[= option.selected ? 'selected': '']]>[[= option.text ]]</option>
    [[ }) ]]
</select>

The last step is to have the phones index list view respond to filter changes.

public/javascripts/views/phones/index_list_view.js

PhonesIndexListView = Backbone.View.extend({

    initialize: function () {
.
.
.
        this.listenTo(this.model, 'change:sortBy', this.rerender);
        this.listenTo(this.model, 'change:query', this.render);
    },

    render: function () {
        var filtered_phones = this.collection.query(this.model.get('query'));
        filtered_phones = _.sortBy(filtered_phones, function (phone) {
            var attr_value = phone.get(this.model.get('sortBy'));
            return _.isString(attr_value) ? attr_value.toLowerCase() : attr_value;
        }, this);
        this.filtered_collection.set(filtered_phones);
    },
.
.
.
    rerender: function () {
        this.filtered_collection.set();
        this.render();
    }
});

And to actually get the phones to order by age, we have to update our data with an age attribute.

public/javascripts/views/phones/index_view.js

PhonesIndexView = Backbone.View.extend({

    initialize: function () {
        this.filter = new Filter();
        this.collection = new PhonesCollection([
            {'name': 'Nexus S',
             'snippet': 'Fast just got faster with Nexus S.',
             'age': 1},
            {'name': 'Motorola XOOM™ with Wi-Fi',
             'snippet': 'The Next, Next Generation tablet.',
             'age': 2},
            {'name': 'MOTOROLA XOOM™',
             'snippet': 'The Next, Next Generation tablet.',
             'age': 3}
        ]);
.
.
.

We can now sort to our heart’s content at localhost:8888
Screen Shot 2014-04-09 at 9.38.52 PM.png

Step-5 API integration #

Reset the work-space to the next step by running

git checkout -f step-5

The full diff can be found on github.

API integration is one area where Backbone really shines. The aim of this section is to issue a request to our web server to fetch our phone model data instead of having it hard coded in the view. Retrieving data in this fashion is commonplace in most production apps. Making this process work is super easy in Backbone. First we need to tell our PhonesCollection where it can expect to find phones list data.

public/javascripts/collections/phones_collection.js

PhonesCollection = Backbone.Collection.extend({
    model: Phone,

    url: '/api/phones',
.
.
.
});

Next, after we instantiate a new phone collection in the view, we need to tell it to fetch its data from the server.

public/javascripts/views/phones/index_view.js

PhonesIndexView = Backbone.View.extend({

    initialize: function () {
        this.filter = new Filter();
        this.collection = new PhonesCollection();
.
.
.
        this.collection.fetch();
    }
.
.
.

Finally, since our phones list data isn’t being pre-populated like before, we have to wait until the server responds with the data before we can render it out.

public/javascripts/views/phones/index_list_view.js

PhonesIndexListView = Backbone.View.extend({

    initialize: function () {
        this.filtered_collection = new PhonesCollection();
        this.listenTo(this.filtered_collection, 'add', this.renderPhone);

        // render when data is returned from server
        this.listenTo(this.collection, 'sync', this.render);

        this.listenTo(this.model, 'change:sortBy', this.rerender);
        this.listenTo(this.model, 'change:query', this.render);
    }
.
.
.

And that’s it. Our phones are now being fetched from our API back-end and displayed at localhost:8888

Step-6 Phone Images and links #

Reset the work-space to the next step by running

git checkout -f step-6

The full diff can be found on github.

Images and links in Backbone are nothing special, we just need to generate the appropriate HTML using the data already in each phone model.

public/javascripts/templates/phones/index_list_phone.jst

<a href="/phones/[[= get('id') ]]" class='thumb'><img src="[[= '/' + get('imageUrl') ]]"></a>
<a href="/phones/[[= get('id') ]]">[[= get('name') ]]</a>
<p>[[= get('snippet') ]]</p>

Here, we are creating the URL of an individual phone by appending its id to the phones path. The approach we usually take is to have our API return an absolute path to the phone show page for us, so all we have to do is display that URL. But, in this case, we are using the data provided by Angular. Same thing goes for the image source.

However, we have one small problem with our links. We have not told Backbone to intercept link click events, so the browser is going to do what it normally does and navigate by doing a full page refresh. Since our app is so simple, we’re just going to intercept and handle the click event where it happens – in our index view.

public/javascripts/views/phones/index_list_phone_view.js

PhonesIndexListPhoneView = Backbone.View.extend({

    events: {
        'click a': 'navigate'
    },
.
.
.
    navigate: function (event) {
        event.preventDefault();
        App.navigate(event.currentTarget.pathname, { trigger: true });
    }
});

After we prevent the event from propagating to the browser, we tell the instance of our Backbone router, App, to use pushState to update the browser history and also trigger any appropriate route event that matches the new URL. We have yet to implement any other routes so right now clicking a link will get us nowhere.

On the upside, since we added images and links, our index page at localhost:8888 now looks nice.
Screen Shot 2014-04-10 at 10.55.48 AM.png.

Step-7 Routing the Phone Show View #

Reset the work-space to the next step by running

git checkout -f step-7

The full diff can be found on github.

We would like to make our phone links work so that clicking a phone on the index page will take you the the correct phone show page. The first step is to create the matching route.

public/javascripts/router.js

Router = Backbone.Router.extend({

    routes: {
        '': 'phonesIndex',
        'phones/:id': 'phonesShow'
    },

    phonesIndex: function () {
        new PhonesIndexView({ el: 'section#main' });
    },

    phonesShow: function (id) {
        new PhonesShowView({
            el: 'section#main',
            model: new Phone({ id: id })
        });
    }
});

Now, whenever the Backbone router detects that the URL has been changed to match phones/ followed by a specific phone id, it will call the phonesShow method and pass in whatever phone id happens to match. The phoneShow method creates a new PhonesShowView, tells it where it is allowed to render its content, and also creates a new phone model. Specifically, the phone model it creates has one attribute assigned to it, its id. Setting this id is important since it allows Backbone to know where to request information about the phone from the server. To make this magic happen we need to specify the beginning part of the URL that our API responds to.

public/javascripts/models/phone.js

Phone = Backbone.Model.extend({
    urlRoot: '/api/phones'
});

With the url route specified in the model constructor, calling fetch on any model instance with an id will automatically send a GET request to the url /api/phones/id, where id is the id that we set in the router.

To get the model data fetching process started, we need to set it up in our phones show view.

public/javascripts/views/phones/show_view.js

PhonesShowView = Backbone.View.extend({

    initialize: function () {
        this.model.fetch();

        this.listenTo(this.model, 'sync', this.render);
    },

    render: function () {
        this.$el.html(JST['phones/show'](this.model));
    }
});

The first thing we do is tell the model to go fetch its data from the API. Next, we attach a listener to that will call the render method whenever the model data is returned from the server.

As an aside, I guess technically we should attach the listener before calling fetch on the model, but since fetch is done asynchronously, the ordering doesn’t matter since the line that defines the listener will be evaluated before any callback can be fired. End aside.

Once the model is fetched from the server, Backbone automatically parses the response so that the model data can be accessed using the model get method. Just to make sure everything is working correctly, let’s create a super simple template that will display the clicked on phone’s id on the screen.

public/javascripts/templates/phones/show.jst

TBD: detail view for [[= get('id') ]]

Now going to the phones index page at localhost:8888 and clicking on the first phone, for example, will display its id.
Screen Shot 2014-04-10 at 11.43.15 AM.png

Step-8 Phone Show View #

Reset the work-space to the next step by running

git checkout -f step-8

The full diff can be found on github.

In this step, our aim is to have the phone show view display all the phone details on the screen. This step has a lot of stuff going on so it is helpful to first examine what we are trying to make
Screen Shot 2014-04-10 at 11.51.44 AM.png

There are different ways to break apart and render complicated views like this one, but the approach we are going to use here is to break it down into one parent view and three child views.

Screen Shot 2014-04-10 at 11.51.44 AM parent-children.png

The first thing we’ll do is to adjust our main show template to display the name and description that appear in the upper right of the main view and to create HTML elements for the child views.

public/javascripts/templates/phones/show.jst

<div class='phone-images'></div>
<h1>[[= get('name') ]]</h1>
<p>[[= get('description') ]]</p>
<ul class='phone-thumbs'></ul>
<ul class='specs'></ul>

Next, we’ll create new child views and assign them their elements.

public/javascripts/views/phones/show_view.js

PhonesShowView = Backbone.View.extend({

    initialize: function () {
        this.model.fetch();

        this.listenTo(this.model, 'sync', this.render);
    },

    render: function () {
        this.$el.html(JST['phones/show'](this.model));

        new PhonesShowImageView({
            el: this.$('.phone-images'),
            model: this.model
        });

        new PhonesShowImagesListView({
            el: this.$('ul.phone-thumbs'),
            model: this.model
        });

        new PhonesShowSpecsView({
            el: this.$('ul.specs'),
            model: this.model
        });
    }
});

Working down the list, the PhonesShowImageView just initializes and renders

public/javascripts/views/phones/show_image_view.js

PhonesShowImageView = Backbone.View.extend({

    initialize: function () {
        this.render();
    },

    render: function () {
        this.$el.html(JST['phones/show_image'](this.model.get('mainImage')));
    }
});

The more interesting question is what is it rendering. The data included with Angular treats images as nothing more than a path to their location on the server. While this is a fine approach, we’re going to promote images from simple strings to full blown Backbone models.

public/javascripts/models/photo.js

Photo = Backbone.Model;

Inspecting further the data that Angular has given us, we notice that one of the Phone’s attributes is a list of images stored in an array. Since we are going to promote photos to Backbone models, a list of photos calls out to be converted to a Backbone collection. Let’s define that collection.

public/javascripts/collections/photos_collection.js

PhotosCollection = Backbone.Collection.extend({
    model: Photo
});

Since our data modeling is now in good shape, we turn to converting the image URLs to Photo models and converting the images array to a PhotosCollection. The place to do these conversions is wherever the data is being returned to us from the server, which in this case is in the Phone model. Backbone provides to a hook to access the server response as a special model method called parse.

public/javascripts/models/phone.js

Phone = Backbone.Model.extend({
    urlRoot: '/api/phones',

    parse: function (res) {
        if (res.images) {
            this.photosCollection = new PhotosCollection(_.map(res.images, function (image_path) { return { path: image_path }; }));
            this.set('mainImage', this.photosCollection.models[0]);
        }
        return res;
    }
});

We directly assign a photos collection to the phone model and convert each of the image strings to Photo models with one attribute called path. We also default the Photo model attribute mainImage to be the first Photo model in the collection. We set it using the set writer method so that changes will be automatically picked up by Backbone and the appropriate events will fire.

Finally, getting back to the template of our PhonesShowImageView

public/javascripts/templates/phones/show_image.jst

<img src="[[= '/' + get('path') ]]" class='phone'>

The next view in our list is the PhonesShowImagesListView. The idea for this view is to render out a thumbnail for each phone photo. Since we have access to the photo collection through our phone model it’s easy to do using the same technique as we illustrated earlier.

public/javascripts/views/phones/show_images_list_view.js

PhonesShowImagesListView = Backbone.View.extend({

    initialize: function () {
        this.render();
    },

    render: function () {
        this.$el.html('');
        this.model.photosCollection.each(this.renderImage, this);
    },

    renderImage: function (photo) {
        this.$el.append(new PhonesShowImagesListImageView({
            tagName: 'li',
            model: photo
        }).el);
    }
});

We first make sure the view’s element is empty and then for each of the photos we create a separate photo model view and append the returned HTML to the list. The photo model view currently does nothing more than render the appropriate HTML for its photo model.

public/javascripts/views/phones/show_image_view.js

PhonesShowImageView = Backbone.View.extend({

    initialize: function () {
        this.render();
    },

    render: function () {
        this.$el.html(JST['phones/show_image'](this.model.get('mainImage')));
    }
});

with corresponding template
public/javascripts/templates/phones/show_images_list_image.jst

<img src="[[= '/' + get('path') ]]">

The final view in our list is the phone’s specs. This view is really simple.

public/javascripts/views/phones/show_specs_view.js

PhonesShowSpecsView = Backbone.View.extend({

    initialize: function () {
        this.render();
    },

    render: function () {
        this.$el.html(JST['phones/show_specs'](this.model));
    }
});

With a corresponding template that is basically just a dump of the phone’s specs.

public/javascripts/templates/phones/show_specs.jst

<li>
    <span>Availability and Networks</span>
    <dl>
        <dt>Availability</dt>
        <dd>[[= get('availability') ]]</dd>
    </dl>
</li>

<li>
    <span>Battery</span>
    <dl>
        <dt>Type</dt>
        <dd>[[= get('battery').type ]]</dd>
        <dt>Talk Time</dt>
        <dd>[[= get('battery').talkTime ]]</dd>
        <dt>Standby time (max)</dt>
        <dd>[[= get('battery').standbyTime ]]</dd>
    </dl>
</li>
.
.
.

In fact, this view is too simple. It doesn’t have a real purpose and probably should just be part of the parent view. The reason we broke it off here was in anticipation of having some nicely formatted details data. We were then going to just loop over the data and programmatically generate all the details, possibly with the use of some string inflector methods. However, the data we’re using from Angular doesn’t lend itself well to that purpose, which is understandable.

Currently our main image view, and image list view suffer from a similar problem of irrelevancy, but we will see the utility of breaking them off in the last section of this tutorial.

First let’s put a little polish on the way our specs look.

Step-9 Phone Show Helper #

Reset the work-space to the next step by running

git checkout -f step-9

The full diff can be found on github.

The problem that we are trying to solve is that some of our specs return a Boolean so the words true and false are being printed on the screen. We speculate that users would rather see a ✓ to represent true and an ✘ to represent false rather then the words themselves. Making this happen is pretty easy. We first define a global phones helper object and give it a checkmark method.

public/javascripts/helpers/phones_helper.js

PhonesHelper = {
    checkmark: function (truthy) { return truthy ? '\u2713' : '\u2718'; }
};

We used a global object to store phone helper methods, but we could have encapsulated the helper if we wanted to in order to to lessen the possibility of name collisions.

The only other thing to do is to use our new helper in the phone specs template.

public/javascripts/templates/phones/show_specs.jst

.
.
.
<li>
    <span>Connectivity</span>
    <dl>
        .
        .
        .
        <dt>Infrared</dt>
        <dd>[[= PhonesHelper.checkmark(get('connectivity').infrared) ]]</dd>
        <dt>GPS</dt>
        <dd>[[= PhonesHelper.checkmark(get('connectivity').gps) ]]</dd>
    </dl>
</li>

<li>
    <span>Display</span>
    <dl>
        .
        .
        .
        <dt>Touch screen</dt>
        <dd>[[= PhonesHelper.checkmark(get('display').touchScreen) ]]</dd>
    </dl>
</li>

<li>
    <span>Hardware</span>
    <dl>
        .
        .
        .
        <dt>FM Radio</dt>
        <dd>[[= PhonesHelper.checkmark(get('hardware').fmRadio) ]]</dd>
        <dt>Accelerometer</dt>
        <dd>[[= PhonesHelper.checkmark(get('hardware').accelerometer) ]]</dd>
    </dl>
</li>
.
.
.

And voilà, ✓’s and ✘’s on our page at localhost:8888/phones/motorola-xoom-with-wi-fi
Screen Shot 2014-04-10 at 5.16.01 PM.png

Step-10 Main Image Swapping #

Reset the work-space to the next step by running

git checkout -f step-10

The full diff can be found on github.

The last order of business is to let the user change out the main image for any thumbnail. We’ll let them swap out the image by clicking on whichever thumbnail image they want to see.

We’re already in good shape since we broke apart our views in step-8 and made our photos full blown Backbone models. The first thing we need to do is trigger an event to let the Phone know which photo it is suppose to replace it’s mainImage with. The natural place to do this is in the photo model view.

public/javascripts/views/phones/show_images_list_image_view.js

PhonesShowImagesListImageView = Backbone.View.extend({
    events: {
        'click': 'selectImage'
    },
.
.
.
    selectImage: function (event) {
        this.model.trigger('imageSelected', this.model);
    }
});

This works by triggering a custom event 'imageSelected' on whichever Photo model was clicked, since we linked the click event to selectImage. We also make sure to pass the entire photo model along as data with the 'imageSelected' event. In Backbone, all model events are automatically trigger on their collection as well, so we can listen to the collection instead of trying to listen to every photo model. We’ll do this in the main image view.

public/javascripts/views/phones/show_image_view.js

PhonesShowImageView = Backbone.View.extend({

    initialize: function () {
        this.listenTo(this.model.photosCollection, 'imageSelected', this.setMainImage);
        this.listenTo(this.model, 'change:mainImage', this.render);

        this.render();
    },
.
.
.
    setMainImage: function (photo) {
        this.model.set('mainImage', photo);
    }
});

Here, we listen for a PhotosCollection 'imageSelected' event and set the main image of the Phone model to be whichever photo model was clicked. Next, the main image is re-rendered since we have a listener monitoring changes to the phone’s main image that calls the view’s render method.

With those additions, clicking a thumbnail photo at localhot:8888/phones/motorola-xoom-with-wi-fi will now replace the main image.
Screen Shot 2014-04-10 at 5.35.34 PM.png

Wrapping-up #

We’ve reach the end of our tutorial. The Angular tutorial contains two more steps – integration with a REST API in step-11 and animations in step-12. We’ve already covered integration with a REST API in step-5 of this tutorial by fetching data through Backbone models and collections. Angular’s animations seem like a great feature, and something that might be fun to do with Backbone, but animations fall outside Backbone’s core area of applicability.

All in all, coding the PhoneCat tutorial in Backbone took 171 lines of JavaScript, whereas the original PhoneCat app coded in Angular only took 48 lines. The extra lines of code in Backbone mostly came from creating a bunch of Backbone views. Backbone requires these extra views because having a well defined view structure greatly facilitates data binding. Angular takes care of most of this behind the scenes with Angular scopes.

I hope you find this tutorial helpful when trying to decide between Angular and Backbone for your next app. Angular seems like a great JavaScript framework and I’m impressed by the brevity of the code. For my next app I’m going to consider using Angular, but I’m really fond of the less restrictive nature of Backbone, so I’ll probably end up sticking with Backbone.

 
25
Kudos
 
25
Kudos

Now read this

The Math of Split Testing Part 1: Statistical Sampling Uncertainty

tl;dr Here we introduce the idea of statistical sampling uncertainty in the content of conversion rate optimization for website design. We begin by illustrating how successive trials of coin flips result in the binomial mass function.... Continue →