Apr 302012
 

Backbone in Baby Steps, part 3

In this part we will cover connecting our Backbone application to to a server through a REST API. This tutorial builds on the first two parts, so I strongly recommend reading those to fully benefit from this part.

Update: If you for some reason don’t have access to a server you could use the Backbone.Localstorage plugin (you’ll need a browser that supports localstorage though). To do this, download the plugin here, add it in a script tag in the html page after Backbone and initialize it in the collection like this:

var Library = Backbone.Collection.extend({
    localStorage: new Backbone.LocalStorage("LibraryStorage"),
    model: Book,
    url: '/api/books'
});

To synchronize a Backbone application to a server we need a server with a REST API that communicates in JSON format. The server I created in part 2.5 will fit our needs, but if you are creating your own server in another language you might want to read through that tutorial to get a grip of the API. Backbone makes use of the sync function in order to persist models with the server. You do not need usually use this function directly, but instead set a url attribute to a collection (or a Model if the Models are not in a collection) informing Backbone where to sync. So lets go ahead and add a url attribute to our Library collection:

    var Library = Backbone.Collection.extend({
        model:Book,
        url:'/api/books'
    });

By this Backbone will assume that the API looks like this:

url HTTP Method Operation
/api/books GET Get an array of all books
/api/books/:id GET Get the book with id of :id
/api/books POST Add a new book and return the book with an id attribute added
/api/books/:id PUT Update the book with id of :id
/api/books/:id DELETE Delete the book with id of :id

To make our application get the Book models from the server on page load we need to update the LibraryView. It is recommended in the Backbone documentation to insert all models when the page is generated on the server side, rather than fetching them from the client side once the page is loaded. Since this tutorial will try to give you a more complete picture of how to communicate with a server, we will go ahead and ignore that recommendation. Go to the LibraryView declaration in app.js and make the following updates:

    var LibraryView = Backbone.View.extend({
        el:$("#books"),

        initialize:function () {
            //this.collection = new Library(books);
            this.collection = new Library();
            this.collection.fetch();
            this.render();

            this.collection.on("add", this.renderBook, this);
            this.collection.on("remove", this.removeBook, this);
            this.collection.on("reset", this.render, this);
        },

Here I have replaced the row where we create a collection from the internal array with this.collection.fetch(). I have also added an listener on the reset event. We need to do this since the fetching of models is asynchronous and happens after the page is rendered. When the fetching is finished, Backbone will fire the reset event, which we listen to and re-render the view. If you reload the page now you should see all books that are stored on the server:

As you can see the date looks a bit weird, and also the keywords. The date delivered from the server is converted into a JavaScript Date object and when applied to the underscore template it will use the toString() function to display it. There isn’t very good support for formatting dates in JavaScript so we will use the dateFormat jQuery plugin to fix this. Go ahead and download it from here and put it in your js folder. Then go ahead and change  index.html like this:

<body>
<div id="books">
    <form id="addBook" action="#">
        <div>
            <label for="coverImage">CoverImage: </label><input id="coverImage" type="file">
            <label for="title">Title: </label><input id="title">
            <label for="author">Author: </label><input id="author">
            <label for="releaseDate">Release date: </label><input id="releaseDate">
            <label for="keywords">Keywords: </label><input id="keywords">
            <button id="add">Add</button>
        </div>
    </form>
</div>
<script id="bookTemplate" type="text/template">
    <img src="<%= coverImage %>"/>
    <ul>
        <li><%= title%></li>
        <li><%= author%></li>
        <li><%= $.format.date(new Date(releaseDate), 'dd/MM/yyyy') %></li>
        <li><%= keywords %></li>
    </ul>
    <button class="delete">Delete</button>
</script>
<script src="js/jquery-1.7.1.js"></script>
<script src="js/jquery.dateFormat-1.0.js"></script>
<script src="js/underscore.js"></script>
<script src="js/backbone.js"></script>
<script src="js/app.js"></script>
</body>

So I added the jquery.dateFormat-1.0.js file on row 25 and used dateFormat to print the date in dd/MM/yyyy format on line 19. Now the date on the page should look a bit better. How about the keywords? Since we are receiving the keywords in an array we need to execute some code that generates a string of separated keywords.  To do that we can omit the equals letter in the template tag which will let us execute code that doesn’t display anything:

<script id="bookTemplate" type="text/template">
    <img src="<%= coverImage %>"/>
    <ul>
        <li><%= title%></li>
        <li><%= author%></li>
        <li><%= $.format.date(new Date(releaseDate), 'dd/MM/yyyy') %></li>
        <li><% _.each(keywords, function(keyobj){%> <%= keyobj.keyword %><% }}); %></li>
    </ul>
    <button class="delete">Delete</button>
</script>

Here I iterate over the keywords array using the each function and print out every single keyword. Note that I display the keyword using the <%= tag. This will display the keywords with a space between. If you would like to support space characters within a keyword, like for example “new age” then you could separate them by comma (no sane person would put a comma in a keyword right?). That would lead us to write some more code to remove the comma after the last keyword like this:

        <li><% _.each(keywords, function(keyobj, index, list){%><%= keyobj.keyword %><% if(index < list.length - 1){ %><%= ', ' %><% }}); %></li>

Reloading the page again should look quite decent:

Now go ahead and delete a book and then reload the page: Tadaa! the deleted book is back! Not cool, why is this? This happens because when we get the BookModels from the server they have an _id attribute (notice the underscore), but Backbone expects an id attribute (no underscore). Since no id attribute is present, Backbone sees this model as new and deleting a new model don’t need any synchronization. To fix this we could change the server response, but we are instead going to look at the parse function of Backbone.Model. The parse function lets you edit the server response before it is passed to the Model constructor. Update the Book model like this:

    var Book = Backbone.Model.extend({
        defaults:{
            coverImage:"img/placeholder.png",
            title:"No title",
            author:"Unknown",
            releaseDate:"Unknown",
            keywords:"None"
        },
        parse:function (response) {
            console.log(response);
            response.id = response._id;
            return response;
        }
    });

I simply copy the value of _id to the needed id attribute. If you reload the page you will see that models are actually deleted on the server when you press the delete button.

Update: A simpler way of making Backbone recognize _id as its unique identifier is to set the idAttribute of the model like this:

    var Book = Backbone.Model.extend({
        defaults:{
            coverImage:"img/placeholder.png",
            title:"No title",
            author:"Unknown",
            releaseDate:"Unknown",
            keywords:"None"
        },
        idAttribute: "_id"
    });

If you now try to add a new book using the form you’ll notice that it is the same story as with delete – models wont get persisted on the server. This is because Backbone.Collection.add doesn’t automatically sync, but it is easy to fix. In LibraryView in app.js, change the line reading:

this.collection.add(new Book(formData));

…to:

this.collection.create(formData);

Now newly created books will get persisted. Actually, they probably wont if you enter a date. The server expects a date in UNIX timestamp format (milliseconds since Jan 1, 1970). Also any keywords you enter wont be stored since the server expects an array of objects with the attribute ‘keyword’. We start with fixing the date issue. We don’t really want the users to enter a date into a specific format manually so we’ll use the standard datepicker from jQuery UI. Go ahead and create a custom jQuery UI download containing datepicker from here . Add the css and js files to index.html:

<head>
    <meta charset="UTF-8"/>
    <title>Backbone.js Web App</title>
    <link rel="stylesheet" href="css/screen.css">
    <link rel="stylesheet" href="css/cupertino/jquery-ui-1.8.19.custom.css">
</head>

and the js file after jQuery:


<script src="js/jquery-1.7.1.js"></script>
<script src="js/jquery-ui-1.8.19.custom.min.js"></script>
<script src="js/jquery.dateFormat-1.0.js"></script>
<script src="js/underscore.js"></script>
<script src="js/backbone.js"></script>
<script src="js/app.js"></script>

Now in the beginning of app.js bind datepicker to our releaseDate field:


(function ($) {
    $( "#releaseDate" ).datepicker();

    var books = [
        {title:"JS the good parts", author:"John Doe", releaseDate:"2012", keywords:"JavaScript Programming"},

You should now be able to pick a date when clicking in the releaseDate field:

Now go to the addBook function in LibraryView and update it like this:

        addBook:function (e) {
            e.preventDefault();

            var formData = {};

            $("#addBook div").children("input").each(function (i, el) {
                if ($(el).val() !== "") {
                    if (el.id === 'releaseDate'){
                        formData[el.id] = $('#releaseDate').datepicker("getDate").getTime();
                    } else {
                        formData[el.id] = $(el).val();
                    }
                }
            });

            books.push(formData);

            //this.collection.add(new Book(formData));
            this.collection.create(formData);
        },

Here I check if the current element is the releaseDate input field, in which case I use datePicker(“getDate”) which will give me a Date object and then use the getTime function on that to get the time in milliseconds. While we are at it lets fix the keywords issue as well:

        addBook:function (e) {
            e.preventDefault();

            var formData = {};

            $("#addBook div").children("input").each(function (i, el) {
                if ($(el).val() !== "") {
                    if (el.id === 'keywords') {
                        var keywordArray = $(el).val().split(',');
                        var keywordObjects = [];
                        for (var j = 0; j < keywordArray.length; j++) {
                            keywordObjects[j] = {"keyword":keywordArray[j]};
                        }
                        formData[el.id] = keywordObjects;
                    } else if (el.id === 'releaseDate'){
                        formData[el.id] = $('#releaseDate').datepicker("getDate").getTime();
                    } else {
                        formData[el.id] = $(el).val();
                    }
                }
            });

            books.push(formData);

            //this.collection.add(new Book(formData));
            this.collection.create(formData);
        },

Here I check if the current element is the keywords input field, in which case I split the string on each comma the create a new array with keyword objects. In other words I assume that keywords are separated by commas, so I better write a comment on that in the form. Now you should be able to add new books with both release date and keywords!

Summary

In this tutorial we made our application persistent by binding it to a server using a REST API. We also looked at some problems that might occur when serializing and deserializing data and their solutions. We looked at the dateFormat and the datepicker jQuery plugins and how to do some more advanced things in underscore template. I hope you enjoyed reading this tutorial series as much as I did creating them. The code is available on github as usual.

  11 Responses to “Backbone in Baby Steps, part 3”

  1. Bravo, man. For some reason, I’ve almost found learning Backbone.js to be more trouble than it’s worth. This tutorial is the only one I’ve made it through without errors and it’s the most thorough (gets you up and running with node, mongodb, mongoose, express as a bonus).

    • True. It worked seamlessly unlike other tutorials which have speed breakers. Good problem statement and coverage.

  2. What about the book cover? PS: Love this tutorial! Thanks!

  3. Best tutorial on Backbone.js . Finally, I finished all three parts and learned the basics of backbone.
    Thanks.. :)

  4. Helped me a lot to understand Backbone.js better. Great set of tutorials!

  5. What if I to edit a model?

  6. Hey, have you seen this? You might enjoy it, Nicholas Zakas describes how yahoo basically made their own version of backbone, wayyy back when, and the architecture decisions behind how to develop such a thing.

    https://www.youtube.com/watch?v=vXjVFPosQHw

  7. Somebody essentially lend a hand to make seriously articles I’d state. This is the first time I frequented your website page and thus far? I amazed with the analysis you made to create this particular submit amazing. Great job!

  8. Where can I find part 1 and 2?

  9. Its a good tutorial to learn backbone begineers with good examples with nodejs and mongodb.

  10. could you show an example og category product list, Ex: first you have a list of categories and when you click on a single category then second view will render products in that category.

    thank you

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>