(Re)Learning Backbone Part 8

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

A Little Cleanup

Back when we created the REST endpoints, we returned some "friendly" success messages. Without a UI, these made it easy to see when the requests went through. Now that we are going to be using them, let's have them return something more useful.

For GET /api/users and GET /api/users/:user_id, we already return an array or a single user, as appropriate. Let's do the same for POST /api/users and PUT /api/users/:user_id. For these, we will return the User that was either just created or was updated. There is no user we can return after a DELETE, so we can return an empty object. Our updated /app/routes/user.js code now looks like this:

var bodyParser = require('body-parser');  
var User = require('../models/user');

module.exports = function (app, express) {  
    var userRouter = express.Router();

    userRouter.get('/', function (req, res) {
        res.json({message: 'api is loaded'});
    });

    userRouter.route('/users')
        .post(function (req, res) {
            var user = new User();
            user.name = req.body.name;
            user.email = req.body.email;
            user.password = req.body.password;

            user.save(function (err) {
                if (err) {
                    if (err.code === 11000) {
                        return res.json({success: false, message: 'Duplicate username.'});
                    } else {
                        return res.send(err);
                    }
                } else {
                    res.json(user);
                }

            });
        })
        .get(function (req, res) {
            User.find(function (err, users) {
                if (err) {
                    res.send(err);
                }
                res.json(users);
            })
        });

    userRouter.route('/users/:user_id')
        .get(function (req, res) {
            User.findById(req.params.user_id, function (err, user) {
                if (err) res.send(err);
                res.json(user);
            })
        })
        .put(function (req, res) {
            User.findById(req.params.user_id, function (err, user) {
                if (err) res.send(err);

                if (req.body.name) user.name = req.body.name;
                if (req.body.email) user.email = req.body.email;
                if (req.body.password) user.password = req.body.password;

                user.save(function (err) {
                    if (err)res.send(err);
                    res.json(user);
                });
            });
        })
        .delete(function (req, res) {
            User.remove({_id: req.params.user_id}, function (err, user) {
                if (err) res.send(err);
                res.json({});
            })
        });

    return userRouter;
};

Adding Actions To Our User List

We're going to want to be able to Add a User, and Delete or Edit a specific user. Let's start with Delete.

Let's add a column to our table on the right side to hold user specific actions. Add a blank <th></th> to our <thead> and a <td></td> in our #each loop. In the <td> add the following:

<button data-id="{{_id}}" class="btn btn-danger btn-sm js-deleteUser">Delete</button>  

This will display a Delete button. We've added a data=id attribute to hold the User id. This will allow us to specify which User to delete. We've also added a js-deleteUser class to the button. We are using the js-* pattern for class names that indicate functionality as opposed to design.

Now, let's add the functionality to perform the delete in /app/users/listView.js. Under the el declaration, add an events block

events: {  
    'click .js-deleteUser': 'deleteUser'
},

On the left, we define an event and a CSS selector for the event, and on the right, a method to handle the event. In this case, we are trapping for a click event on any item with a class of js-deleteUser, and delegating to the deleteUser method to handle it. Let's create that method now.

deleteUser: function(e){  
    var self = this;
    var id = $(e.currentTarget).attr('data-id');
    var user = this.collection.get(id)
    user.destroy().done(function(){
        self.collection.remove(user);
        self.render();
    });
}

Here we ask the object that triggered the event for the value of its data-id attribute, and retrieve that user from our collection. We trigger the destroy method provided by Backbone, we will call our REST backend. When it is complete, we remove the user from the collection, and redraw the table.

Reload the app, and delete a user. If you don't have any to delete, you can either use the Postman extension to add some more, or, we can forge ahead and wire in an Add button.

We want to have a modal dialog popup with a form that will allow us to enter in a name, an email, and a password, along with a button to save it. With a little bit of configuration, we can use this same dialog to edit an existing user. To do the heavy lifting, we're going to add in a few libraries. Since we are using Bootstrap for our CSS, let's just dive in and grab the whole framework. We'll still only pull in what we need as we need it. We're also going to use a Backbone plugin to wrap the Bootstrap modal functionality so that we can easily interact with it from our views. Adding backbone.bootstrap-modal with bower will pull both libraries in.

We also want to incorporate data-binding. Data-binding is one of the big features that frameworks like Angular and Ember pitch as a benefit. We're going to get all the functionality they provide with much greater and explicit control of when we use binding and how for a lighter footprint on our page. For this, we will use stickit from the NYTimes. Install both these libraries with

bower install backbone.bootstrap-modal backbone.stickit --save  

and update the require-main.js file

"bootstrap-modal": "../components/bootstrap/js/modal",
"backbone.bootstrap-modal": "../components/backbone.bootstrap-modal/src/backbone.bootstrap-modal",
"stickit" : "../components/backbone.stickit/backbone.stickit"

Let's once again update our list view, with an Add button at the top of the page and an Edit button next to the Delete.

/app/users/list.hbs

<div class="well">  
    <h4><b>Users</b><span class="pull-right"><button class="btn btn-primary js-editUser">Add User</button></span></h4>
</div>  
<table class="table table-bordered table-striped">  
    <thead>
    <th>ID</th>
    <th>Email</th>
    <th>Name</th>
    <th></th>
    </thead>
    <tbody>
    {{#each users}}
        <tr>
            <td>{{_id}}</td>
            <td>{{email}}</td>
            <td>{{name}}</td>
            <td>
                <div class="pull-right">
                    <button data-id="{{_id}}" class="btn btn-danger btn-sm js-deleteUser">Delete</button>
                    <button data-id="{{_id}}" class="btn btn-success btn-sm js-editUser">Edit</button>
                </div>
            </td>
        </tr>
    {{/each}}
    </tbody>
</table>  

Since we are using the same code for both editing and adding, we've added the same class, js-editUser to both. To the Edit button, we add a data-id attribute. If we were going to add any more actions, we might consider putting the ID on the row once for all the fields. For now, we'll stick with this.

Let's update our List view to trap for the new event.

events: {  
    'click .js-deleteUser': 'deleteUser',
    'click .js-editUser': 'editUser'
},

and

editUser: function(e) {  
    var id = $(e.currentTarget).attr('data-id');
    var options = id ? {title: 'Edit User', model: this.collection.get(id)} : {title: 'Add User'};
    console.log('options', options);
}

We don't have anywhere to send the user just yet, so we'll construct our options and log them to verify our event is wired in. Here we check for a data-id value on the element. If we have one, we are going to edit the user, so we set an appropriate title for our soon to be created dialog, and retrieve the user from the collection. If there is no data-id, like on the Add button, we set the title to 'Add User', and leave the model undefined. Reload your app and verify that we have all this working.

Creating our Dialog

Whether we are adding or editing a User, we will need a form with fields to enter data. Let's create a template for one, wrapped in the markup to create a Bootstrap modal dialog.

/app/users/single.hbs

<div class="modal-dialog">  
    <div class="modal-content">
        <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
            <h4 class="modal-title"><b>{{title}}</b></h4>
        </div>
        <div class="modal-body">
            <form class="form-horizontal">
                <div class="form-group">
                    <label for="email" class="col-lg-2 control-label">Email</label>

                    <div class="col-lg-10">
                        <input type="text" class="form-control" id="email" placeholder="Email">
                    </div>
                </div>
                <div class="form-group">
                    <label for="name" class="col-lg-2 control-label">Name</label>

                    <div class="col-lg-10">
                        <input type="text" class="form-control" id="name" placeholder="Name">
                    </div>
                </div>
                <div class="form-group">
                    <label for="password" class="col-lg-2 control-label">Password</label>

                    <div class="col-lg-10">
                        <input type="password" class="form-control" id="password" placeholder="Password">
                    </div>
                </div>
            </form>
        </div>
        <div class="modal-footer">
            <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
            <button type="button" class="btn btn-primary js-saveUser">Save changes</button>
        </div>
    </div>
</div>  

This is mostly boilerplate Bootstrap code. Notice that we have assigned IDs to the form fields. We will use these to bind our data. Because we are binding our data, we don't need to use any template substitution for these values. We have also added a variable to hold our title at the top, and we added a js-saveUser class to our Save button. We'll use Bootstrap's built in functionality to cancel/close the modal when the user hits the Cancel or Close buttons.

To pair with our template, we will create a simple Backbone view. This view will handle binding our model data to the form fields and vice versa. It will also persist the data when the Save button is pressed.

/app/users/singleView.js

define(function (require) {

    'use strict';
    var Backbone = require('backbone');
    var mediator = require('mediator');
    var template = require('hbs!users/single');

    require('stickit');

    var User = require('users/model');

    return Backbone.View.extend({
        initialize: function (options) {
            this.title = options ? options.title : 'Please Set A Title';
            this.model = this.model || new User();
        },

        bindings: {
            '#name': 'name',
            '#email': 'email',
            '#password': 'password'
        },

        events: {
            'click .js-saveUser': 'saveUser'
        },

        render: function () {
            this.$el.html(template({title: this.title}));
            this.stickit();
            return this;
        },

        saveUser: function () {
            var self = this;
            this.model.save().done(function (response) {
                if (self.model.isNew()) {
                    self.model.set('_id', response._id);
                }
                mediator.trigger('users:userSaved', self.model);
            });
        }
    });

});

At the top, we require our dependencies. We also include stickit here. Since it is a plugin, we don't need to assign it to anything. In our initialize method, we set some default values if the options passed to us don't have them. We also create a new User if we need one, as is the case for when we are adding one.

The bindings block is new. This is how we configure stickit. In this simple example, we map our CSS selectors on the right to User model attributes on the left. Stickit can do a lot more than this so read the docs.

We set up a standard events entry to trap for the Save button.

In our render method, we render our template as normal, then call stickit to activate the bindings.

Finally, we create a saveUser method. We use Backbone's built in save functionality. This will determine if the User is new or being edited, and call the appropriate backend method. When we get a successful response, we update our object with an _id value if necessary, then fire off a message to let our list view know we are done. We do this so that the list view can manage the lifecycle of the dialog window, including the opening and closing of it.

Let's now go back to our list view and update it to manage the dialog window.

Add our new dependencies

var mediator = require('mediator');  
var SingleView = require('users/singleView');  
require('bootstrap-modal');  
require('backbone.bootstrap-modal');  

Since we are going to be responding to message, let's wire that up:

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

bindPageEvents: function(){  
    mediator.on('users:userSaved', this.userSaved, this);
},

userSaved: function(user){  
    this.modal.close();
    this.collection.add(user);
    this.render();
}

When we receive a message that the user has been saved, we close the modal that we will soon create, add the passed user to our collection, and rerender the table. If the user has an id that is already part of our collection, it will update itself, otherwise it will add itself.

Finally, lets create the modal. Let's go back to our editUser method

editUser: function(e) {  
    var id = $(e.currentTarget).attr('data-id');
    var options = id ? {title: 'Edit User', model: this.collection.get(id)} : {title: 'Add User'};
    var modalView = new SingleView(options);
    this.modal = new Backbone.BootstrapModal({content: modalView}).open();
},

We've removed the logging. We instantiate the view that holds our form fields with the options we've created. We then wrap that view with a Backbone.BootstrapModal view and open it. Simple as that. Reload your app and try adding and editing some Users.

Let's make one final cosmetic tweak. Since the last column in the table is always the same visually, let's turn sorting off for it. Pass in the following options when we construct the DataTable

this.$('table').DataTable({  
    "aoColumns": [
        null,
        null,
        null,
        { "bSortable": false }
    ]
});

Next Steps

We now have a relatively full featured CRUD management tool. We could easily add new entities by following the patterns we have set up here. Could we have generated these much quicker with an all encompassing framework? Sure. The use of small focused plugins, however, has kept our boilerplate code to a minimum and we have had full control over every line of code.

The last big piece that we are going to add that I consider mandatory for a starter app is authentication. We will put all the User editing functionality behind a login screen and see how to hold and use application wide data, such as the currently logged in user.

(The code at this state in the project can be checked out at commit: 8a19651c576968b29f143d271147b0b288ca63a7)