Apr 052012
 

Backbone in Baby Steps, Part 2

In the first part 1 covered some Backbone basics of models, views and collections. I recommend reading that before continuing since this tutorial builds on the first part. In this tutorial we are going to look at how we can add and remove models from a collection.

Adding models

To add a new book we need to have some sort of input form. Go ahead and add one in index.html

<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" type="text" />
 <label for="author">Author: </label><input id="author" type="text" />
 <label for="releaseDate">Release date: </label><input id="releaseDate" type="text" />
 <label for="keywords">Keywords: </label><input id="keywords" type="text" />
 <button id="add">Add</button></div>
</form></div>

I made the ids of the inputs match the attributes in our Book model so that we don’t have to remap those later. Add this to screen.css for looks

#addBook label {
    width:100px;
    margin-right:10px;
    text-align:right;
    line-height:25px;
}

#addBook label, #addBook input {
    display:block;
    margin-bottom:10px;
    float:left;
}

#addBook label[for="title"], #addBook label[for="releaseDate"] {
    clear:both;
}

#addBook button {
    display:block;
    margin:5px 20px 10px 10px;
    float: right;
    clear: both;
}

#addBook div {
    width: 550px;
}

#addBook div:after {
    content:"";
    display:block;
    height:0;
    visibility:hidden;
    clear:both;
    font-size:0;
    line-height:0;
}

Now lets create a function that lets us add a book. We put it in our master view (LibraryView) after the render function

addBook: function(){
    var formData = {};

    $("#addBook").children("input").each(function(i, el){
        formData[el.id] = $(el).val();
    });

    books.push(formData);

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

Here I select all the input elements the form and iterate over them using jQuerys each. Since we used the same names for ids in our form as the keys on our Book model we can simply store them directly in the formData object and the add it to the books array. We then create a new Book model and add it to our collection. Now lets wire this function to the Add button in our form. We do this by adding an event listener in the LibraryView

events:{
    "click #add":"addBook"
},

By default, Backbone will send an event object as parameter to the function. This is useful for us in this case since we want to prevent the form from actually submit and reloading the page. Add a preventDefault to the addBook function

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

    var formData = {};

    $("#addBook").children("input").each(function(i, el){
        formData[el.id] = $(el).val();
    });

    books.push(formData);

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

Ok, so this will add a book to our books array and our collection. It will not however be visible, because we have not called the render function of our BookView. When a model is added to a collection, the collection will fire an add event. If we listen to this event we will be able to call our renderBook function. This binding is done in initialize.

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

    this.collection.on("add", this.renderBook, this);
},

Now you should be ready to take the application for a spin.

As you may notice, if you leave a field blank, it will be blank in the created view as well. This is not what we want, we would like the default values to kick in. To do that we need to add a bit of logic. Also note that the file input for the cover image isn’t working, but that is left as an exercise to the reader :)

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

    var formData = {};

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

    books.push(formData);

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

Here I added (line 7) a check to see if the field value is empty, in which case we do not add it to the model data. While we are at it lets add default values for the other properties of Book

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

Now it has better default behaviour

Removing models

Now lets see how to remove Books. We start by adding a delete button to the template in index.html

<script id="bookTemplate" type="text/javascript">
    <img src="<%= coverImage %>"/>
<ul>
	<li><%= title%></li>

	<li><%= author%></li>

	<li><%= releaseDate%></li>

	<li><%= keywords%></li>

</ul>

    <button class="delete">Delete</button>
</script>

We add some css to it for good looks. Note that I removed the margin of the existing ul rule above to tighten things up a bit.

.bookContainer ul {
    list-style-type: none;
    margin-bottom: 0;
}

.bookContainer button {
    float:right;
    margin: 10px;
}

Looks ok

Now we need to wire up the button to the logic. This works in the same way as with add. We start by creating a deleteBook function in the BookView

var BookView = Backbone.View.extend({
    tagName:"div",
    className:"bookContainer",
    template:$("#bookTemplate").html(),

    render:function () {
        var tmpl = _.template(this.template); //tmpl is a function that takes a JSON and returns html

        this.$el.html(tmpl(this.model.toJSON())); //this.el is what we defined in tagName. use $el to get access to jQuery html() function
        return this;
    },

    deleteBook:function () {
        //Delete model
        this.model.destroy();

        //Delete view
        this.remove();
    }
});

Then we add en event listener to the delete button

var BookView = Backbone.View.extend({
    tagName:"div",
    className:"bookContainer",
    template:$("#bookTemplate").html(),

    render:function () {
        var tmpl = _.template(this.template); //tmpl is a function that takes a JSON and returns html

        this.$el.html(tmpl(this.model.toJSON())); //this.el is what we defined in tagName. use $el to get access to jQuery html() function
        return this;
    },

    events: {
        "click .delete": "deleteBook"
    },

    deleteBook:function () {
        //Delete model
        this.model.destroy();

        //Delete view
        this.remove();
    }
});

If you try it out now you will see that it seems to work. However we are not yet finished. When we click the delete button the model and view will be deleted, but not the data in our books array. This means that if we were to re-render the LibraryView the deleted books would reappear. The Backbone collection is smart enough to notice when one of its models is deleted and will fire a “remove” event. This is something we can listen to in our LibraryView and take action. Add a listener in initialize of LibraryView

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

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

Here I specified that the removeBook function should be called when the remove event from our collection fires, so lets create this function. Note that the collection provides the removed model as a parameter to the event.

removeBook: function(removedBook){
    var removedBookData = removedBook.attributes;

    _.each(removedBookData, function(val, key){
        if(removedBookData[key] === removedBook.defaults[key]){
            delete removedBookData[key];
        }
    });

    _.each(books, function(book){
        if(_.isEqual(book, removedBookData)){
            books.splice(_.indexOf(books, book), 1);
        }
    });
},

Since the default values was not saved in the books array we need to remove them in order to find an match. We use underscores each function to iterate over the properties of the removed Book model and delete any property that is equal to the default value. Since the underscore each function also is capable of iterating over objects in an array we use it again to iterate over the objects in our books array to find the data of the removed Book. To get a match we use the isEqual function of underscore that performs a deep comparison of objects. Similarly the indexOf can find complex objects, which we use when we remove the book data using splice.

Summary

In this tutorial we have looked at how to add and remove models from a collection. We also looked at how to wire view, model and collection together using event handlers. We also saw the useful underscore functions each, indexOf and isEqual. If there is enough interest in these tutorials I’ll continue and create more episodes, there are still plenty to cover in Backbone.

The code for this part is available on github.

  14 Responses to “Backbone in Baby Steps, part 2”

  1. Hey, This is by far the best backbone tutorial I’ve encountered. I like the step by step process and backing it up with explanations.

    I am planning to head jump in to BIBS 2.5

    Thanks.

  2. Greatest tutorial ever! Thanx man

  3. It doesn’t seem that your selector for iterating through all the input fields is working. My guess is because .children only looks at the direct descendants when in fact the input fields are not direct descendants.

    • I had this problem too, solved assigning an id to the div after form id=”addBook” and iterating through its children.

  4. Why do you have those pre tags in the form html at the top of this page?

  5. Why do you have the “CDATA” stuff for the delete button?

  6. Delete is not really working the way as mentioned. On refresh, the deleted data still reappears.

    • i think because you still have the Books array in the app.js file i guess when it reloads it reloads the table with it

  7. Hey! Absolutely wonderful series, but one caveat. If you had used this.model instead of this.collection for your library view, you wouldn’t need the additional removebook method in your library view; it would have been deleted automatically when the model was removed in the sub view.

    Great, great read, thank you!

  8. At the point in your tutorial where you said “Now you should be ready to take the application for a spin.” I tried the page and not matter what I entered into the form I would get the default values to show up. I read and re-read the steps so far, typed and re-typed, even copied and pasted from your examples. All I was getting was the default values.

    I thought I’d read on and hoped my error would become clear. In the next section you say “if you leave a field blank, it will be blank in the created view as well”. Which wasn’t what I was getting at all.

    It wasn’t until I got to the next block of code that I saw this:


    $("#addBook div").children("input").each(function (i, el)

    In the first steps you had this


    $("#addBook").children("input").each(function(i, el)

    So the “div” was missing! Because it works as you described now. I’m not sure if that was left out by mistake, or it was a test to your readers. Regardless, this is a great tutorial, and I am slowly getting a hang of backbone. Thanks.

    • that’s true dude ! it worked for me now
      but i don’t know why we have to add the div even though it worked when i added the div
      in
      ” $(“#addBook div”).children(“input”).each(function(i, el){}”

  9. why do i have to click twice in delete button to make te action happen???

 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>