Build a Contacts Manager Using Backbone.js: Part 4

Build a Contacts Manager Using Backbone.js: Part 4

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

Final Product What You'll Be Creating

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

In part four of this series, we saw how easy it is to add and remove models from our collection, and keep the page updated in sync with the changes. In this part, we’re going to look at editing existing model data.


Getting Started

We’ll start out by adding another simple button to the template, which will enable editing of its data:

<button class="edit">Edit</button>

As we are adding this button to our existing template, we can also add an entirely new template that can be used to render an editable form in which the model data can be changed. It’s very similar to the exiting template, and can be added to the page after the existing template:

<script id="contactEditTemplate" type="text/template">
<form action="#">
    <input type="file" value="<%= photo %>" />
    <input class="name" value="<%= name %>" />
    <input id="type" type="hidden" value="<%= type %>" />
    <input class="address" value="<%= address %>" />
    <input class="tel" value="<%= tel %>" />
    <input class="email" value="<%= email %>" />
    <button class="save">Save</button>
    <button class="cancel">Cancel</button>
</form>
</script>

The new template consists mostly of <input> elements that expose the editable data. We don’t need to worry about labels for the elements, but instead use the data from the model as the default values of each input. Note that we’re using a hidden form field to store the type attribute of the model, we’ll use this to set the value of a <select> that we need to add using our script instead of having the template render it.

Next we can bind some event handlers for the new buttons we’ve added; update the events object in the ContactView class so that it contains the following new bindings:

"click button.edit": "editContact",
"change select.type": "addType",
"click button.save": "saveEdits",
"click button.cancel": "cancelEdit"

Don’t forget to add the trailing comma to the end of the existing binding! These are very similar to the bindings we’ve used before; each key:value pair simply specifies an event to listen for and a selector to match the element that triggers the event as the key, and the event handler to execute on detection of the event as the value.


Switching a Contact Into Edit Mode

In the same way that we stored a reference to the template function under the template property of our ContactView class, we should also store a reference to the template function that we’ll use to switch the contact into edit mode. Add editTemplate directly after the template property:

editTemplate: _.template($("#contactEditTemplate").html()),

Now we can add the event handlers themselves, which should also go into the ContactView class after the existing deleteContact() method. First, we’ll add the editContact() method:

editContact: function () {
    this.$el.html(this.editTemplate(this.model.toJSON()));

    var newOpt = $("<option/>", {
        html: "<em>Add new...</em>",
        value: "addType"    
    }),

    this.select = directory.createSelect().addClass("type")
        .val(this.$el.find("#type").val()).append(newOpt)
        .insertAfter(this.$el.find(".name"));

    this.$el.find("input[type='hidden']").remove();
},

We start out by rendering our new editTemplate that we added to the page using Underscore’s template() method in the same way that we added each contact using the standard display template.

In order to make editing the type of contact easier we can render a select box that lets the user switch easily between existing types, but we also want to cater for the possibility that the user may want to add a new type. To allow for this, we’ll create a special option for the select box with the text Add new... and a value of addType.

We then create the new <select> element using the createSelect() method of our master view, which if you recall from the last part in this tutorial will return a <select> element containing an <option> for each unique type in the collection. We give it a class name, and to get the <select> element to show the existing type of the contact being edited we set its value to the value of the hidden <input> we added in our template. We then insert the new <select> after the <input> for the contact’s name. The new select element is added as a property of the view instance so that we can interact with it easily.

Once we’ve added the <select> element for the contact’s type, we can then remove the hidden field so that it doesn’t interfere with saving the edit, which we’ll look at shortly.

At this point, we should now be able to click the edit button in any of our contacts and have the contents of that contact converted into a form:


Adding a New Type

One of the event bindings we added was for the change event of the type select box, so we can add a handler which replaces the <select> box with a standard <input> element:

if (this.select.val() === "addType") {
    this.select.remove();

    $("<input />", {
        "class": "type"
    }).insertAfter(this.$el.find(".name")).focus();
}

When the <select> element’s value changes we first check whether its value is addType and if so, we remove the element from the page and create a new <input> element to replace it. We then insert the new element using jQuery’s insertAfter() method and focus it ready for text entry.


Updating the Model

Next we can add the handler that will take the changes made in the edit form and update the data in the model. Add the saveEdits() method directly after the editContact() method that we just added:

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

    var formData = {},
        prev = this.model.previousAttributes();

    $(e.target).closest("form").find(":input").add(".photo").each(function () {

        var el = $(this);
        formData[el.attr("class")] = el.val();
    });

    if (formData.photo === "") {
        delete formData.photo;
    }

    this.model.set(formData);

    this.render();

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

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

First of all, we create an empty element to store the data that has been entered into the form, and also store a copy of the previousAttributes of the model that belongs to the view we’re working with. The previousAttributes property of models is a data store that Backbone maintains for us so that we can easily see what an attribute’s previous attribute data was.

We then get each input element from the form using a combination of jQuery’s find() method and the :input filter, which gives us all of the form fields. We don’t want the cancel or save <button> elements though, so we remove them from the selection using jQuery’s not() method.

Once we have our collection of fields, we iterate over them using jQuery’s each() method and for each item in the collection, we add a new key to our formData object using the current item’s class, and a new value using the current item’s value.

When we convert the editable contact back into a normal contact, we don’t want to lose the default photo if a new photo has not been chosen. To make sure we don’t lose the default photo, we can delete the photo property from our formData object if its value is blank.

Backbone models have a setter method that can be used to set any attribute.

Backbone models have a setter method that can be used to set any attribute. In order to update the model’s data we just call its set() method passing in the formData object that we have prepared. Once this is done we call the view’s render() method and our newly updated model will be rendered back to the page, with any updated information from the form.

As we have done previously, we need to update the data stored in our original contacts array so that filtering the view doesn’t lost any changes we have made. We do this in a very similar way as before, first checking whether the photo property has the default value and removing it if so, and then using a combination of Underscore’s each() and isEqaul() methods to find the item in the contacts array that has changed. This is where we use the previousAttributes that we saved earlier; we can’t use the current model anymore because its attributes have just been updated.

We use the native JavaScript’s splice() function to update the contacts array. As before, we obtain the index of the item to update using Underscore’s indexOf() method as the first argument to splice() and set the function to update a single item using the second argument. This time we supply our formData object as the third argument. When splice() receives three (or more) arguments, the third argument is the data to replace the data that has just been removed.


Cancelling the Edit

We have one button left that we need to add a handler for – the cancel button. This method will be very simple and will just switch the contact back into non-edit mode, using the original data from the model. Add this method after the saveEdits() method:

cancelEdit: function () {
    this.render();
},

That’s all we need to do! We already have a method that takes a model and renders it as a view on the page, so we simply call this method and the original model data will be used to recreate the original contact. This is useful because even if someone changes the data in the form fields while the contact is in edit mode, when the cancel button is clicked, these changes will be lost.


Summary

In this part of the tutorial we’ve looked at how we can update the data of an existing model rather than creating a whole new model. To do this we essentially just need to call a model’s set() method and pass in the new attributes that we wish to set.

As with Backbone however, we’ve only covered a small fraction of what these libraries provide, there is so more that we can use when building complex applications on the front-end.

As we saw however, we also need to think about how we can change the view to allow the visitor to enter the data that will be set as the new attributes. In this example, we achieved this by creating another template to handle rendering a form pre-filled with the existing attribute data which the user can overtype to change.

Over the course of this series, we’ve looked at all of the major components of Backbone including Models, Collections, Views and Routers and Events. We’ve also looked at some of the methods and properties provided by Backbone that we can use to interact with the different constructs to produce an integrated, functioning application, albeit a basic one.

As well as learning some Backbone basics, one of the most important aspects of the tutorial was in how the application is structured, with all of our code organised in a logical and consistent way. Applications written in this style can be much easier to return to and maintain on the long-term. Much of our functionality was event driven, either in response to the actions of the visitor in the form of UI event handlers, but some were also driven by changes to the collection and triggered manually at the appropriate point in our code.

We’ve also looked at some of the utilities provided by Underscore which has given us easy ways to work with the objects and array that form the foundation of our application. As with Backbone however, we’ve only covered a small fraction of what these libraries provide, there is so more that we can use when building complex applications on the front-end.

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

    Article is missing an excerpt, messing up the frontpage !

    • http://pressedweb.com Cory

      This. Entire article is on the frontpage. :O

  • http://www.cleantags.com sergio

    will the author continue the series or the backbone.js tutorials? I want to see some more advanced tutorials of cool things to do with backbone

  • Amit Erandole

    Hey Dan,

    I hope the series isn’t over. You still need to show us how to save and sync with server side technologies like ruby or php. Also how does one override Backbone.sync so it can work with non-rest based frameworks like codeigniter? Please cover these topics.

    Really enjoying this series so far btw.

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

      Glad you’re enjoying it so far :) I’m working on the final part, which covers syncing right now! Stay tuned…

  • http://www.techcalling.com Ryan

    Note that you need to explicitly add a function called addType to the contact view.

    addType: function() {
    if (this.select.val() === ‘addType’) {
    this.select.remove();

    $(”, {
    class: ‘type’,
    }).insertAfter(this.$el.find(‘.name’)).focus();
    }
    }

  • http://www.techcalling.com Ryan O’Donnell

    Also

    $(e.target)
    .closest(“form”)
    .find(“:input”)
    .add(“.photo”)
    .each

    is supposed to actually be

    $(e.target)
    .closest(‘form’)
    .find(‘:input’)
    .not(‘:button’) //dont want to select the buttons
    .each

  • http://organic-web.net David

    thanks for this it’s been great to see backbone in action. would be great to see it working with local storage. Would also be cool to see this app in spine.js too :)

  • Dan Smart

    Could you tell me how Backbone.js compares to Monorail.js (https://github.com/runexec/Monorail.js) ?

  • erminio ottone

    Love the series, i hope i’ll see more backbone tuts in nettuts in future

  • http://aiccomunicacoes.com Ivon Matos

    nossa muito bom eu tava precisando mesmo muito obrigado, pela postagem…

  • rye

    When editing the contact type (friend, colleague, etc.), you wouldn’t want to have the type “All” in there. I’ve removed it like so:

    this.select = directory.createSelect().children(‘:first’).remove().end().addClass(“type”)
    .val(this.$el.find(“#type”).val()).append(newOpt)
    .insertAfter(this.$el.find(“.photo”));

  • http://www.nonstopcars.com nice cars

    Thanks the series, but It’s hard for beginners like me.

  • http://vincentmac.com Vincent Mac

    Thanks for the great tutorial Dan.

    I’ve created a github repo to go along with this tutorial. As of right now, it is current up to part 4.
    I’ve made a couple of minor modifications (things like correcting a couple of typos where the written tutorial varies from the source) and added some of the other recommendations other people have made in the comments.

    I have also updated the saveEdits function to so that it re-renders the select filter. Previously, if you added a new type for a contact, it would not show up in the filter.

    Github Repo: https://github.com/vincentmac/ContactManager

    • http://vincentmac.com Vincent Mac

      I forgot to mention the biggest change I made…

      This is a Node.js (with Express.js) implementation of this tutorial

      • rowild

        Thank you! – I am reading the comments from bottom to top, so this may be with that will come up. I realized 2 things:

        1. When entering the edit mode from a filtered view (like “friend”), the type select box won’t show all types

        2.Upon saving edited data, the Master View should respect a possible change of the filter type and re-render the view according to the new one …

  • http://shohadanews.mihanblog.com خبرهای روز

    Thanks Dan, so useful series of tutorial.
    I’m one of those who continuing this article from the step one.
    The library seems so great to me, thanks for covering it up.

  • Gavin

    @Vincent – thanks for your addition to the select type filter, I was wondering how to do this and my own tries yielded nothing initially. I checked yours out to see where I went wrong – Thanks!

  • pinksy

    At the end of the saveEdits function, it’s also worth adding the following, to update the types dropdown if a new type is added:

    if (this.select.val() === “addType”)
    $(directory.el).find(“#filter”).find(“select”).remove().end().append(directory.createSelect());

  • http://AndrewHenderson.me Andrew Henderson

    I really appreciate you creating these tutorials, however they do not work when following the instructions. The download files work, but the code differs from the steps provided.

    • http://klonowski.me Karol

      Yup, I agree. You have to check the demo’s source code to make the app

      for example, in ‘Adding a new type’ part of the tutorial, you forgot the (addType : function() {… } ) part

      or, when you said ‘We don’t want the cancel or save elements though, so we remove them from the selection using jQuery’s not() method.’ you’re not actually filtering with ‘.not()’ in the code here, while you did it in the demo ;)

      These are just tiny things to point out. Nevertheless, you’re doing an excellent job – it’s the only tutorial I’ve managed to find that is really comprehensive and gives a good overview of the framework.

      Thanks very much for all your work, I really learnt a lot ;)

      cheers

  • http://jasonjamora.com Jason

    This code triggers an error

    this.select = directory.createSelect().addClass(“type”).val(this.$el.find(“#type”).val()).append(newOpt).insertAfter(this.$el.find(“.name”));

    Error is..
    Uncaught SyntaxError: Unexpected identifier

    Can anyone help?

    • Charles

      It’s because his code sample, as with almost all his code samples, is wrong. Use this instead:

      this.$el.html(this.editTemplate(this.model.toJSON()));

      //add select to set type
      var newOpt = $(“”, {
      html: “Add new…“,
      value: “addType”
      });

      this.select = directory.createSelect().addClass(“type”).val(this.$el.find(“#type”).val()).append(newOpt).insertAfter(this.$el.find(“.name”));
      this.$el.find(“input[type='hidden']“).remove();

      Alos, you’ll get another error because he tells us to declare the events before the functions exist. Just comment out the events until you get to the parts where you implement them…

      This has been a painful experience. However, I actually think I learned more since I had to rewrite almost all of it!

    • http://khaynote.com/ K

      This is how your code like, right?

      var newOpt = $("<option/>", {
              html: "<em>Add new...</em>",
              value: "addType"   
          }),
       
          this.select = directory.createSelect().addClass("type")
              .val(this.$el.find("#type").val()).append(newOpt)
              .insertAfter(this.$el.find(".name"));
      

      Yes, there is one error. At the end of variable declaration, ( var newOpt ) must end with “;” , not with comma “,” . Like this one!

      var newOpt = $("<option/>", {
              html: "<em>Add new...</em>",
              value: "addType"   
          });
      

      LOL, I also had a hard times for that, too. ^^

    • http://khaynote.com K

      This is how your code like, right?

      var newOpt = $(“<option/>”, {
      html: “<em>Add new…</em>”,
      value: “addType”
      }),

      this.select = directory.createSelect().addClass(“type”)
      .val(this.$el.find(“#type”).val()).append(newOpt)
      .insertAfter(this.$el.find(“.name”));

      Yes, there is one error. At the end of variable declaration, ( var newOpt ) must end with “;” , not with comma “,” . Like this one!

      var newOpt = $(“”, {
      html: “<em>Add new…</em>”,
      value: “addType”
      });

      LOL, I also had a hard times for that, too. ^^

  • http://twitter.com/KevLawrence Kevin Lawrence

    Grabbing form elements by their class seems like a bad idea. What if I want to add more classes to these elements? It’d break everything I’d imagine.

  • johndurbinn

    Man, Backbone is a little hard to pick up. Very difficult to see what the “right” way of doing something is…

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

    One thing I was noticed if you are filtered and try editing any contact you cant change the type to any of the types except the selected filtered type and all.

  • David de Lusenet

    Hello! I managed to update the select with this code found below:

    if(this.select.val()==="addType") {

    $(directory.el).find("#filter").find("select").remove().end().append(directory.createSelect());

    }

    The select updates correctly, but if you select the new type the contact isn’t shown. Anybody got any ideas to fix this? Thanks!

  • oluwaseun

    This is a rather awesome Backbone tutorial for newbies typos/errors or not. There were errors and typos no doubt but the general structure of Backbone was explained and the examp[le made it easier. Even though I will advice the writers to take more time to proof read their materials. It will save your readers some headache.
    I have a question. When you add a new type, the select box is not updated. I guess an event should be triggered to that effect. I should think Backbone will trigger and update event. Will check.

    Thanks though.

    • oluwaseun

      Okay, I think I found the solution from the comments. Add this snippet to the code at the last line in your saveEdits i.e. just after

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

      add this line now

      if (this.select.val() === “addType”){
      $(directory.el).find(“#filter”).find(“select”).remove().end().append(directory.createSelect());
      }

  • rowild

    Thanks for the tutes! – It seems to get quite a bit messy, though. Honestly, I do not see the point where the OOP structure offers a better organisation here.

    For example: in the model, there comes a point when the master view needs to be addressed. This is done by calling “directory” – which is an instance of a Collection View object. What happens, if I instantiate another master view called something else like “directoryTwo”? All those references within the objects to “directory” will fail for my 2nd instance. And that is not OOP.

    Also, the whole trouble about updating the select box seems to need a complete separate solution approach. While adding a contact from the “Master View”, the function responsible for updating the select box resides within that Master View class, which is fine. But as soon as it comes to updating the select box from within the edit mode – which is within a single model view and NOT the Master view – the function is actually not available, and even if it is made available (by hardcoding the “directory” instance var into the Single View), the context of the function call gets messed up.

    Don’t get me wrong, Dan! I really, really appreciate those tutorials, and – even though there are complaints – it seems they are even more valuable BECAUSE of the mistakes – which I see as test. They really make you learn that stuff, and I think I wouldn’t have that insight now without the “errors” and the comments.

    Nevertheless I wonder if there is any chance for a tutorial that discusses different approaches and problem solutions using this very contact manager.

    Thank you and thanks to the helpful community!

    • rowild

      Ahem… I wonder why my post is here somewhere in the middle of the others? Isn’t the list sorted by date?

  • http://twitter.com/jams_o_donnell Jonathan Sutcliffe

    I found that if I filtered the contacts before editing only the filtered types were available to select from. I’ve modified the code slightly to make it draw the full list of types, with a new method on the DirectoryView master view, getAllTypes(), and a parameter on createSelect(). I’m starting to suspect that there’s no reason to ever create a select with anything other than the full list of types in this application.

    getAllTypes: function(){ // new method
    var fullDirectory = new Directory(contacts);
    return _.uniq(fullDirectory.pluck(‘type’), false, function(type){
    return type.toLowerCase();
    });
    },
    createSelect: function(full){ // note new parameter, a boolean
    var filter = this.$el.find(‘#filter’),
    select = $(”, {
    html: ‘All’
    });
    full ? types = this.getAllTypes() : types = this.getTypes(); // check whether we want to get all types
    _.each(types, function(item){
    var option = $(”, {
    value: item.toLowerCase(),
    text: item.toLowerCase()
    }).appendTo(select);
    });
    return select;
    },

  • Steff

    I think the prev = this.model.previousAttributes(); statement should be after this.model.set(formData); because before the model.set the previousAttributes seems to be empty. Unfortunally on my apps it works only if I make this patch.

    What do you think about this ?

    • Steff

      Sorry for double post but it’s in the saveEdits function of the ContactView view.