Try Tuts+ Premium, Get Cash Back!
Build a Contacts Manager Using Backbone.js: Part 3

Build a Contacts Manager Using Backbone.js: Part 3

Tutorial Details
  • Applications Used: Backbone.js, jQuery
  • Difficulty: Intermediate
  • Completion Time: 30 Minutes

Final Product What You'll Be Creating

This entry is part 3 of 5 in the Getting to Know Backbone.js Session
« PreviousNext »

Welcome to part three of our series that focuses on building applications using Backbone. If you haven’t read parts one and two, I strongly recommend that you do — just so you know where we’re at and what we’ve covered so far.

In part one, we took a basic look and models, views and collections. In part two, we looked at routers, events and the history modules. In this part, we’re going to look further at interactions and see how we can add or remove models from a collection.


Adding Models to a Collection

If you cast your mind back to part one, you’ll remember how we added all of our models to the collection when the collection was initialised. But how can we add individual models to a collection after the collection has already been initialised? It’s actually really easy.

We’ll add the ability for new contacts to be added which will involve an update to the underlying HTML and our master view. First, the HTML; add the following mark-up to the contacts container:

<form id="addContact" action="#">
    <label for="photo">photo:</label><input id="photo" type="file" />
    <label for="type">Type:</label><input id="type" />
    <label for="name">Name:</label><input id="name" />
    <label for="address">Address:</label><input id="address" />
    <label for="tel">Tel:</label><input id="tel" />
    <label for="email">Email:</label><input id="email" />
    <button id="add">Add</button>
</form>

This simple form will enable users to add a new contact. The main point is that the id attributess of the <input> elements match the attribute names used by our models, which makes it easier to get the data in the format we want.

Next, we can add an event handler to our master view so that the data into the form can be harvested; add the following code after the existing key:value pair in the events object:

"click #add": "addContact"

Don’t forget to add the trailing comma to the end of the existing binding! This time we specify the click event triggered by the element with an id of add, which is the button on our form. The handler we are binding to this event is addContact, which we can add next. Add the following code after the filterByType() method from part two:

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

    var newModel = {};
    $("#addContact").children("input").each(function (i, el) {
        if ($(el).val() !== "") {
            newModel[el.id] = $(el).val();
		}
    });

    contacts.push(formData);

    if (_.indexOf(this.getTypes(), formData.type) === -1) {
       	this.collection.add(new Contact(formData));
        this.$el.find("#filter").find("select").remove().end().append(this.createSelect()); 
    } else {
        this.collection.add(new Contact(formData));
    }
}

As this is an event handler, it will automatically receive the event object, which we can use to prevent the default behaviour of the <button> element when it is clicked (which would be to submit the form and reload the page – not what we want). We then create a new empty object, and use jQuery’s each() method to iterate over each <input> element in our addContact form.

In the callback function supplied to each(), we first check that the field has had text entered into it and if so, we add a new property to the object with a key equal to the id of the current element, and a value equal to its current value. If the field is empty, the property will not be set and the new model will inherit any defaults that may have been specified.

Next, we can update our local data store with the new contact. This is where we would probably save the new data to the server — if we had a server in place to receive such requests. At this point we don’t, so we’ll just update the original array for now so that if the view is filtered, the new data is not lost. All we need to do then is use the collection’s add() method to add the new data to the collection. We can create the new model to pass into the collection within the call to add().

Lastly, we need to update the <select> element so that if the new contact has a different type, that type is available for filtering. However, we only want to re-render the <select> if a new type has been added. We can use Underscore’s indexOf() method to search through an array for a particular value. Like the native JavaScript indexOf() method for strings, this method will return -1 if the value is not found. We pass the array to search as the first argument to indexOf(), and the value to look for as the second.

If the value is not found, the type specified must be new so we find the existing select box and remove it before appending a new one generated by our createSelect() method. If the type is found, we can just add the new model without needing to re-render the select.


Rendering the New Model

Now that we’ve added a new model to the collection, we should render it on the page. To do this we can bind another handler, this time to listen for the add event. Add the following line of code to the initialize() method of the collection:

this.collection.on("add", this.renderContact, this);

We use the on() method once more to attach the event listener and as we already have a method that creates and displays individual views, we just specify that function as the handler. We also set the master view as the this object within the handler as we did with previous handlers. At this point, we should now be able to complete the form, and have the new contact rendered to the page:

One thing to note is that if the addContact form fields are left completely blank, the resulting model will be almost entirely devoid of attributes, which will cause problems when we try to manipulate the model later on. One way to avoid this is to provide defaults for the majority of model attributes, just like we provided the default photo attribute. If there are no sensible defaults we can use, like for a contact’s name for example, we can just supply an empty string. Update the defaults object in the Contact class to include defaults for our other attributes:

name: "",
address: "",
tel: "",
email: "",
type: ""

Deleting Models From the Collection

Now that we know how to add models to the collection, we should look at how they can be removed as well. One way that we could enable deleting of individual models is by adding a delete button to each contact, so this is what we’ll do; first we need to update the template for each individual view so that it contains a delete button. Add a new button to the end of the template:

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

That’s all we’ll need for this example. The logic to remove an individual model can be added to the view class that represents an individual contact, since the view instance will be associated with a particular model instance. We’ll need to add an event binding and an event handler to remove the model when the button is clicked; add the following code to the end of the ContactView class:

events: {
    "click button.delete": "deleteContact"
},

deleteContact: function () {
	var removedType = this.model.get("type").toLowerCase();

    this.model.destroy();

    this.remove();

    if (_.indexOf(directory.getTypes(), removedType) === -1) {
        directory.$el.find("#filter select").children("[value='" + removedType + "']").remove();
    }
}

We use the events object to specify our event binding, as we did before with our master view. This time we are listening for click events triggered by a <button> that has the class name delete. The handler bound to this event is deleteContact, which we add after the events object.

First we store the type of the contact that we just deleted. We should make this value lowercase as we did before to ensure there are no case issues when the contacts viewer is in use.

We then call the destroy() method on the model associated with this, the instance of the view. We can also remove the HTML representation of the view from the page by calling jQuery’s remove() method, which has the added bonus of cleaning up any event handlers attached to the view.

Finally, we get all of the types of the models in the directory collection and check to see if the type of the contact that was just removed is still contained within the resulting array. If it isn’t, there are no more contacts of that type and we should therefore remove that option from the select.

We select the element to remove by first finding the select box, then using an attribute selector to select the <option> with a value attribute that matches the removedType variable that we saved at the start of the method. If we remove all of the contacts of a certain type and then check the <select> element, we should find that the type is no longer in the drop-down:


Removing the Model’s Data

Ok, that subheading is a bit misleading; what I mean is that as well as removing the model and the view, we should also remove the original data in our contacts array that the model was originally built from. If we don’t do this, the model that was removed will come back whenever it is filtered. In a real-world application this is probably where we would sync with a sever in order to persist the data.

The functionality to remove the item from the original array can reside within our master view; the collection will fire a remove event when any of the models are removed from the collection, so we can simply bind a handler for this event to the collection in the master view. Add the following line of code directly after the existing bindings:

this.collection.on("remove", this.removeContact, this);

You should be quite familiar with this statement by now, but as a reminder, the first argument of the on() method is the event we are listening for, the second is the handler to execute when the event occurs, and the third is the context to use as this when the handler is executed. Next we can add the removeContact() method; after the addContact() method add the following code:

removeContact: function (removedModel) {
    var removed = removedModel.attributes;

    if (removed.photo === "/img/placeholder.png") {
        delete removed.photo;
    }

    _.each(contacts, function (contact) {
        if (_.isEqual(contact, removed)) {
            contacts.splice(_.indexOf(contacts, contact), 1);
        }
    });
}

Backbone helpfully passes our handler the model that has just been removed from the collection. We store a reference to the collection of attributes so that we can compare the model that has been removed with the items in our original contacts array. The original items in the contacts array didn’t have the photo property defined, but as this is specified as a default property, all of our models will inherit the property and will therefore fail any comparison with the objects in the contacts array.

In this example, we need to check whether the photo property of the model is the same as the default value, and if it is, we remove the photo property.

Once this is done we can iterate over each item in the contacts array and test it to see if it is the same as the model that was removed from the collection. We can compare each item with the object we store in the removed variable using Underscore’s isEqual() method.

If the isEqual() method returns true, we then call the native JavaScript splice() method on the contacts array, passing in the index of the item to be removed, and the number of items to remove. The index is obtained using Underscore’s indexOf() method that we used earlier on.

Now when a delete button is clicked, the view, model and original data will be erased from existence. We can also filter the view then go back to the view of all contacts, and the contact that was removed will still not be displayed.


Doing Something With the Form

So, we kinda just dumped the addContact form onto the page there didn’t we? To close off this part of the tutorial, we can do something to keep it hidden until a link is clicked. We can add the following link to the <header> element:

<a id="showForm" href="#">Add new contact</a>

To make the link show the form, we’ll need to hide it first and then use a UI event handler to show it. The binding can be added to the events object in the DirectoryView class:

"click #showForm": "showForm"

Our showForm() method can be as simple as follows (although you’ll probably want to do a bit more with it than we do here!):

showForm: function () {
    this.$el.find("#addContact").slideToggle();
}

Summary

In this tutorial, we looked solely at how new models can be added to a collection and how models can be removed from a collection. We saw that the Backbone methods used to add and remove models are, unsurprisingly, the add() and remove() methods.

We also saw how we can bind handlers to the events that are fired automatically when these methods are used in order to update the UI and collection as necessary.

We also looked at some more helpful Underscore utility functions that we can use to work with our data, including _indexOf() which returns that index of an item in an array, and isEqual() which can be used to deep-compare two objects to see if they are identical.

Like in the last part of this tutorial, we also saw how can write our classes in such a way in that their functionality can be shared and reused whenever possible. When we added a new model for example, we made use of the existing renderContact() method defined in our DirectoryView class to handle rendering the HTML for the new contact.

So we’ve seen how to add models and remove them, join me in the next part of this series where we’ll look at how to edit existing model data.

Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • Yavin

    Thank for good tutorial! Please correct link to demo, it has a typo

    • http://www.ssiddharth.com Siddharth

      Fixed, thanks!

  • Ryan

    Maybe link to Parts 1 & 2?

    • Javier

      Idem! Y U NO put links to other parts?

  • Lecksfrawen
  • staras

    How the demo implementation add the contact?

  • http://www.cleantags.com sergio

    great tutorial so far, but please show us how to work with ajax data and json because most of us will create CRUD with ajax

  • erminio ottone

    Thanks! hope to see more backbone.js on nettuts in future :)

  • http://www.leftware.net Jhonny

    the addContact function is incorrect.

    you create and then populate the object newModel… it should be named “formData”

    Great tutorial, thanks

    • http://www.danwellman.co.uk Dan Wellman

      Well spotted! Last minute variable name change ><

  • http://www.thevectorbox.com The Vector Box

    Great tutorial, I will give this a go this evening, this is very explanatory.

  • Robert

    The demo seems to have a bug with building the select. When a filter is applied on the original data (let’s say ‘family’ and you then add a new contact with a new type, the select list will rebuild with only All, Family and the new type.

    • http://www.danwellman.co.uk Dan Wellman

      Weird, the demo works for me; I can create a new contact of the type test and the select menu has all the original types plus the new one…

      • Penelope Yen

        I’ve reproduced the above issue as well… All of the types do show up if you add the new type without any filters applied, but the problem seems to arise upon adding a new type while the types are filtered.

    • http://martinansty.co.uk Martin Ansty

      Seems to be because the getTypes() method checks the current collection, even when it has been filtered. In stead it should reference the original contacts array as this will always have all of the contacts in it (including the newly added one)

      • http://martinansty.co.uk Martin Ansty

        Some code for that:

        //getTypes now uses the contacts array, rather than the directory collection
        getTypes: function(){
        return _.uniq(_.pluck(contacts, ‘type’), false, function(type){
        return type.toLowerCase();
        });
        },

        //as a result we now have to push the new item to the contacts array within the if statement in addContact()
        if(_.indexOf(this.getTypes(), newModel.type) === -1){
        this.collection.add(new Contact(newModel));
        contacts.push(newModel);
        this.$el.find(‘#filter’).find(‘select’).remove().end().append(this.createSelect());
        }
        else {
        contacts.push(newModel);
        this.collection.add(new Contact(newModel));
        }

    • Andrea Gherardi

      I would go for something like this, to add the new filter to our <select>

      —–

      var select = this.$el.find(“#filter”).find(“select”);

      var newFilter = $(“<option/>”, {
      value: newModel.type.toLowerCase(),
      text: newModel.type.toLowerCase()
      }).appendTo(select);

      —–

      this fixes any issue with the filter, as it doesn’t recreate the select every time, but it just adds an option to it…
      anyway, the point here is getting to know backbone, and this is not really related to it :)

  • http://themeforest.net/item/autum-multipurpose-html-template/1694773?ref=UniquePixelWeb UniquePixelWeb

    Great tutorial!

  • http://photosbyluiscamberos.com Luis Camberos

    Great tutorial Dan! Is there any way of adding new contacts and making the data persistant?

    • http://www.danwellman.co.uk Dan Wellman

      There will be a new part coming soon with details on how to persist data to a server :)

      Thanks for reading!

      • http://photosbyluiscamberos.com Luis Camberos

        Excellent! Thanks

  • http://www.wbase.es Wbase

    Hola buenos días,

    Somos una empresa de Diseño Web en Barcelona, Madrid y les queríamos dar las gracias, por vuestra ayuda.

    Muchas Gracias,

    Saludos

  • http://vladnicula.com Vlad Nicula

    Good introduction to backbone! I wonder if the author can write a part that involves working with backbone sync and how the REST features function. The thing that keeps me away from backbone is the lack of a clear example on how to manage server data. By server data I mean big JSON objects, probably updated at a given interval.

    Right now, the contact object is a “global” object. When filtering, if I’m not mistaken, the main collections gets stripped of some entities. This is great for this little example, but in a real world scenario those contacts come from the server – which implies the need to store them separately -. This situation rises a question : Should I use a new collection that is not “bound” to the view, and copy from it based on the filtering rules, or what?

  • http://www.numbertank.com Dhamresh

    Thanks!

    This is very good sample of backbone

  • http://wonderlandlabs.com Dave Edelhart

    I think your addContact function has a few issues. First off you switch from newModel to formData right in the middle. Second, if you are already viewing a subset of records, when you update the types, you will get all, (current filter), (new fileter) in the select dropdown because you don’t have the full collection to poll from.

    I suggest resetting the collection in the beginning of the call (or at least before you recreate the select input) and being consistent with the variable names.

    addContact: function(e){
    e.preventDefault();
    this.collection.reset(contacts, { silent: true });
    var newModel = {};
    $(‘#addContact’).children(‘input’).each(function(i, el){
    if ($(el).val() != ”){
    newModel[el.id] = $(el).val();
    }
    });

    contacts.push(newModel);

    this.collection.add(new Contact(newModel));
    if (_.indexOf(this.getTypes(), newModel.type === -1)){
    this.$el.find(‘#filter’).find(‘select’).remove().end()
    .append(this.createSelect());
    }
    this.filterType = newModel.type.toLowerCase();
    this.filterByType();
    },

    • http://www.danwellman.co.uk Dan Wellman

      Hey Dave,

      The newModel/formData issue was a simple mistake caused by me changing the name of the variable as noted above. But, resetting the collection is a good call and would help mitigate the inconsistencies :)

      • Symen

        Too bad the newModel/formData error has been pointed out in the comments at least three times, but the tutorial still hasn’t been updated. You could have saved some people some frustrations :D

      • belvedere

        No kidding! I’m having to go through the comments at the end of each of these articles to make sure I’m not going crazy…

      • http://twitter.com/joshbedo Josh bedo

        Just did the same thing because the tutorial was different then the source the last two times.

    • rowild

      Small correction:

      if (_.indexOf(this.getTypes(), newModel.type === -1)){

      needs to be

      if (_.indexOf(this.getTypes(), newModel.type) === -1){

  • http://wonderlandlabs.com Dave Edelhart

    I think there is an architectural weakness throughout this demo. Whenever operations are done to change the filtered view, the current viewed set are stored in the collection. However the collection is polled repeatedly to determine which type values to use to populate the select’s options.

    It seems like you need to either have two collections – one of the entire population, and another of the currently viewed set – or reduce the current viewed set to a method, rather than storing it in a collection. That way you can test for inclusion, poll for types, etc. on a collection that represents the entire base population without worrying about its membership being changed thorough ui filtering.

  • http://net.tutsplus.com/tutorials/javascript-ajax/build-a-contacts-manager-using-backbone-js-part-3/ Abhishek Kumar

    Hi any one can tell me

    How can be upload a file on server, if I’m using a PHP script at server side.

    • oluwaseun

      I’m sure if you do a google search you should see loads of code snippets that can help you achieve that.

  • https://github.com/OClement/SocketStreamBackbone Olivier Clement

    Great series of tutorials, most probably the best out there to get newcomers started.

    I have a few questions though, as I’m kind of stuck right now:

    Note that I’m following these tutorials using NodeJs, SocketStream (which allows among other things to “require” modules on the client as per CommonJS’s requires method).

    In order to make things clearer, I keep my models and views in separate files/modules and use ‘ var = requires() to load whatever is needed.

    Considering that I have a main ‘app.coffee’ that instantiate directory = new Directory().

    When I get into the view of a contact, and want to have a reference to the instance’s variable, I obviously can’t as they’re living in two different context. My other concern about this: Don’t using an instance name in a view defeat the purpose of Backbone and OOP? This way you just tie the view to a specific scenario no?

    Any idea on how to address this issue properly?

    For more details on what I mean, here’s a link to my gitHub repo:
    https://github.com/OClement/SocketStreamBackbone

    Refer to ‘/client/code/app’ for Backbone specifics. Sorry it’s all in Coffeescript :P

    Thank you – sorry for not being more concise in my explanations, english is not my primary language.

  • http://www.perisicdesigns.com Stevo

    I had issues with the c:\fakepath comig up when adding an image for the contact, so here is how I solved it:

    newModel.photo = ‘images/’ + newModel.photo.substr(12);

    Use whatever the filepath to your image is, instead of ‘images/’ ….

  • yun

    I don’t know why the following code I wrote will lose the filter functionality simply replacing the two “find()”s with one find() .

    this.$el.find(“#filter select”).remove().end().append(this.createSelect());

    this.$el.find(“#filter”).find(“select”).remove().end().append(this.createSelect());

    Does anyone can tell me the reason? thanks a lot.

    • rowild

      This is, how I think it works:
      The “find” hold the classes it finds. If the first “find” holds “#filter select” the subsequent remove() will delete the #filter container. Therefore it is necessary to do the search in to steps (because if there wasn’t a find(‘#filter’), all “select” tags would get deleted

  • yun

    The model could not be removed from the contacts because the removed model has the default attributes with blanks is not contained in the contacts.

  • https://twitter.com/wanghq wanghq

    I like how you explain the code in a line by line way. Very helpful!

  • alonisser

    in the delete method: I think better than typing the default photo property again – you should just reference it.

    maybe like this

    if(removed.photo === Contact.prototype.defaults['photo'])

  • alonisser

    or maybe even iterating over properties in order to check empty ones:

    for (var attr in removed){
    if(removed[attr] === Contact.prototype.defaults[attr]){
    delete removed[attr];
    }
    }

  • TP

    if (_.indexOf(this.getTypes(), formData.type) === -1) {
    this.collection.add(new Contact(formData));
    this.$el.find(“#filter”).find(“select”).remove().end().append(this.createSelect());
    } else {
    this.collection.add(new Contact(formData));
    }

    ……………

    u call –> this.collection.add(new Contact(formData)); in both if and else cases … (*facepalm*).
    Don’t learn newbies such obviously bad practices…

  • David Crossman

    Great tutorial! But is there a way to delete multiple models at the same time?

  • lye

    It occurred to me when I got to the end of this and you bound the event of a click of the link to toggling the form that maybe most of this could be done with jquery. Now I’m kind of confused as to how much of this could be abstracted out to jQuery, it’s starting to feel like a substitute rather than a compliment.

    • lye

      Seriously though – great tutorials. I actually like that there’s errors in the code because it makes me go back and think about it.

  • Jake

    Extremely disappointing net.tutsplus tutorial… sloppy and full of errors.