(Re)Learning Backbone Part 7

(Note: The code for this project at this point in our progress can be downloaded from Github at commit: e55dbc2be4ab5e8ce883ad7e397654e346f5d135)

Getting Our Users and Displaying Them

Today, we are going to access our REST backend using Backbone's Collection object and display the results on screen. To do this, we'll add a link to our navigation section in the header, create a view to display the results, and update the router to include those results in our page layout.

Navigation

Let's go ahead and update /app/page/header.hbs'. Between the navbar-header div and the navbar-right div, add

<ul class="nav navbar-nav">  
    <li><a href="#users"><span class="fa fa-users"></span> Users</a></li>
</ul>  

You'll notice there are some 'fa' classes in there. That's because I added FontAwesome to the mix. Use bower to install it

bower install font-awesome --save  

then add the CSS to your index page

<link rel="stylesheet" href="components/font-awesome/css/font-awesome.css">  

For our navigation, we used a standard A anchor tag with a URL that mimics our REST API URL. There is no technical need to keep them the same (and in fact they are not - our UI does not expose the /api portion) but keeping them aligned is a useful organizational tool. You should also notice that the URL begins with a hash. Backbone, by default, uses Hash URLs. They are more backwards compatible with older browsers that do not support Push State. Normal URLs are preferred and we'll convert this one later on to take advantage of them.

Creating our Model and Collection

Backbone provides some base objects that we can use to contain our model object (a User) and a collection of them. By giving these objects a URL, they will know how to interact with a standard REST API to retrieve and persist the objects. Let's create them now.

/app/users/model.js

define(function (require) {

    'use strict';
    var Backbone = require('backbone');

    return Backbone.Model.extend({
        idAttribute: '_id',
        urlRoot: '/api/users'
    });
});

Here we are setting two properties, idAttribute, which lets Backbone know to use the MongoDB generated value _id as the id for the object, and urlRoot, which tells it the base URL to use when constructing REST calls.

/app/users/collection.js

define(function (require) {

    'use strict';
    var Backbone = require('backbone');
    var Model = require('users/model');

    return Backbone.Collection.extend({
        model: Model,
        url: '/api/users'
    });
});

Again, we are setting two properties. model is the Backbone model of the objects in the collection - in our case the User model. url is the REST API url to retrieve the collection from.

Displaying The Collection

In my apps, I have two different ways to think about displaying a collection of objects. In cases such as our User collection, it is primarily a tabular display of data. It can show all or a portion of the model's properties, and usually presents actions such as Edit or Delete. In cases such as these, I don't expect the collection to be changing underneath the user without explicit interaction.

Other collections, however, could be more dynamic. Suppose you are looking at a live stream of tweets that are getting continually updated. Perhaps the collection represent pieces in a game that are getting moved around based on rules. In cases like that, we will want to construct a collection view that delegates a lot of control to individual model views and can respond to events that they generate.

For CRUD screens like the one we are going to build, we can use the DataTables library to easily add features like sorting, pagination, and filtering to our view.

Install DataTables and a convenient plugin to mix it in with our Bootstrap theme:

bower install datatables datatables-bootstrap3-plugin --save  

update require-main.js with the following entries

"datatables": "../components/datatables/media/js/jquery.dataTables",
"datatables-bootstrap3": 
    "../components/datatables-bootstrap3-plugin/media/js/datatables-bootstrap3"

and add the CSS to index.html

    <link rel="stylesheet" href="components/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css">

Now let's create our template and view

/app/users/list.hbs

<div class="well">  
    <h4><b>Users</b></h4>
</div>  
<table class="table table-bordered table-striped">  
    <thead>
    <th>ID</th>
    <th>Email</th>
    <th>Name</th>
    </thead>
    <tbody>
    {{#each users}}
        <tr>
            <td>{{_id}}</td>
            <td>{{email}}</td>
            <td>{{name}}</td>
        </tr>
    {{/each}}
    </tbody>
</table>  

A fairly straightforward table template. We'll use Handlebars #each directive to loop over the users and display a row for each.

/app/users/listView.js

define(function (require) {

    'use strict';
    var Backbone = require('backbone');
    var template = require('hbs!users/list');
    require('datatables');
    require('datatables-bootstrap3');

    return Backbone.View.extend({
        el: '#content',
        render: function(){
            this.$el.html(template({users: this.collection.toJSON()}));
            this.$('table').DataTable();
            return this;
        }
    });

});

Some differences to call out. We've required our DataTables related libraries but did not assign them to a variable. Since they are plugins to jQuery, we won't need to access them on their own.

In our render method, we take our collection of users, convert them to JSON and pass that in as users to our template to render. This is the object that the #each directive is looping on in our template.

Finally, we activate the DataTables functionality by attaching it to the table in our view.

The only thing that may not be clear is where the collection we convert to JSON is coming from since we did not define it in this view. We could have instantiated it here, and asked it to fetch itself. We would then have to coordinate with our Page view to tell it that the collection was ready, and to rerender it. In some cases, such as when many different backend calls will need to be made to display a page, this will be an appropriate solution. For this simple case, we'll have the router grab the collection and prepare the view before we hand it off to the main Page. This way, it will be ready to render right away. Let's take a look at that:

/app/router.js

routes: {  
    '': 'home',
    'home': 'home',
    'users': 'users'
},

users: function() {  
    console.log('routing - users');
    require(['users/collection', 'users/listView'], function(Collection, View) {
        var collection = new Collection();
        collection.fetch().done(function(){
            mediator.trigger('page:displayView', new View({collection: collection}));
        });
    });
},

We've added a route for users that is handled by a users method and the users method itself. We require the users view and collection. We instantiate the collection and fetch the contents. When the contents are retrieved we add them a new view and send a message off to the page to render the view.

Next Steps

In the next installment, we'll some actions to add, edit, and delete users.