Diving into CanJS: Part 3

Diving into CanJS: Part 3

Tutorial Details
  • Language: JavaScript
  • Framework: CanJS

Final Product What You'll Be Creating

This is the final part of a three part tutorial that will teach you how to build a contacts manager application in JavaScript, using CanJS and jQuery. When you’re done with this tutorial, you’ll have all you need to build your own JavaScript applications using CanJS!

In part two, you created the Views and Controls needed to display categories, created Model.List helpers, used routing to filter contacts and modified your EJS to take advantage of live binding.

In this part of the tutorial, you will:

  • Edit and delete contacts using the Contact Control
  • Create a Control and View to create contacts
  • Listen to DOM and Model events using Control’s templated event handlers

You’ll be adding to the source files from part one and two so if you haven’t done so already, catch up by reading part one and two.


Updating a Contact

In part one, contactView.ejs placed each property of a contact is in an input tag. To update a contact when these inputs change, you’ll have to add some event handlers to the Contact Control. Add this code contacts.js inside the Contacts Control:

'.contact input focusout': function(el, ev) {
  this.updateContact(el);
},
'.contact input keyup': function(el, ev) {
  if(ev.keyCode == 13){
    el.trigger('blur')
  }
},
'.contact select change': function(el, ev) {
  this.updateContact(el)
},
updateContact: function(el){
  var contact = el.closest('.contact').data('contact');
  contact.attr(el.attr('name'), el.val()).save();
}

Let’s go through this code line-by-line and see how it works:

'.contact input focusout': function(el, ev) {
  this.updateContact(el);
},

Calls updateContact() when any <input> loses focus.

'.contact input keyup': function(el, ev) {
  if(ev.keyCode == 13){
    el.trigger('blur')
  }
}

Triggers the blur event on an <input> if the enter key is pressed while it has focus. This will cause the input to lose focus, which is handled by the focusout event handler.

'.contact select change': function(el, ev) {
  this.updateContact(el)
},

Calls updateContact() when the value of the <select> changes.

var contact = el.closest('.contact').data('contact');

Finds the closest <li> parent tag and retrieves the model instance using $.data().

contact.attr(el.attr('name'), el.val()).save();

Updates the contact using attr(). The name of each <input> matches a property of contact, so el.attr('name') will return the name of the property that is being updated. save() is used to save the change to the Contact Model.


Deleting a Contact

There is a small link with an ‘X’ in the top right corner of each contact. When this is clicked, the contact should be deleted. To do this, add another event handler to the Contacts control that looks like this:

'.remove click': function(el, ev){
  el.closest('.contact').data('contact').destroy();
}

When the X is clicked, the contact instance is retrieved from the nearest <li> and destroy() is called. destroy() deletes the contact from the Model and removes it from any Model.Lists.

Live binding will automatically update your UI when a contact is deleted.

Creating a Contact

Now you’ll create the Control and View needed to create a contact. First you’ll need a giant “New Contact” button. Add this code to index.html right above <div id="filter">:

<a class="btn btn-large btn-primary" href="javascript://" id="new-contact">
  <i class="icon-plus icon-white"></i> New Contact
</a>

You’ll also need to create a new View that will render a form for creating a contact. Save this code as createView.ejs in your views folder:

<div class="hero-unit contact span8">   
  <%== can.view.render('views/contactView.ejs', {
    contact: contact, categories: categories
  }) %>    
  <div class="row">     
    <div class="buttons pull-right">        
      <a href="javascript://" class="btn btn-primary save">Save</a>       
      <a href="javascript://" class="btn cancel">Cancel</a>     
    </div>    
  </div>  
</div>  

This View renders the contactView.ejs sub-template and adds “Save” and “Cancel” buttons. Here’s what it looks like in the application:

Create View

Now you’ll need to create a new Control named Create that will display the form and save the new contact to the Contact Model. Add this code to contacts.js:

Create = can.Control({
  show: function(){
    this.contact = new Contact();
    this.element.html(can.view('views/createView.ejs', {
      contact: this.contact,
      categories: this.options.categories
    }));
    this.element.slideDown(200);
  },
  hide: function(){
    this.element.slideUp(200);
  },
  '.contact input keyup': function(el, ev) {
    if(ev.keyCode == 13){
      this.createContact(el);
    }
  },
  '.save click' : function(el){
    this.createContact(el)
  },
  '.cancel click' : function(){
    this.hide();
  },
  createContact: function() {
    var form = this.element.find('form'); 
      values = can.deparam(form.serialize());

    if(values.name !== "") {
      this.contact.attr(values).save();
      this.hide();
    }
  }
});

Let’s go over this Control in detail to see what’s going on:

show: function(){
  this.contact = new Contact();
  this.element.html(can.view('views/createView.ejs', {
    contact: this.contact,
    categories: this.options.categories
  }));
  this.element.slideDown(200);
},

Creates a empty contact using new Contact({}) and assigns it to this.contact. The new contact is passed to can.view() along with the categories to be rendered.

hide: function(){
  this.element.slideUp(200);
},

Slides the form up out of view.

'.contact input keyup': function(el, ev) {
  if(ev.keyCode == 13){
    this.createContact(el);
  }
}

Calls createContact() if the enter key is pressed while in one of the inputs.

'.save click' : function(el){
  this.createContact(el)
},

Call createContact() when the “Save” button is clicked.

'.cancel click' : function(){
  this.hide();
},

Calls hide() when the “Cancel” button is clicked.

var form = this.element.find('form'); 
  values = can.deparam(form.serialize());

Finds the <form> element and uses jQuery’s serialize() function to get a string representing all the form’s values. Then the serialized string is converted to an object using can.deparam().

if(values.name !== "") {
  this.contact.attr(values).save();
  this.hide();
}

If the name of the contact is not empty, attr() is used to update the contact stored in this.contact. save() is called to save the changes to the model and the form is hidden by calling hide().


Using Templated Event Handlers

Controls also support templated event handlers that allow you to customize an event handler and listen to events on objects other than this.element.

You customize the handler behavior using {NAME} in the event handler. The variable inside the curly braces is looked up on the Control’s this.options first, and then the window. You could create multiple instances of the same Control but customize the behavior of its event handlers in each instance.

Controls can also bind to objects other than this.element using templated event handlers. If the variable inside {NAME} is an object, Control will bind to that object to listen for events. The object does not have to be a DOM element, it can be any object like a Model. To listen to a click anywhere on a page you would use: '{document} click'. as your event handler.

These handlers will get cleaned up when the Control instance is destroyed. This is critical for avoiding memory leaks that are common in JavaScript applications.

Showing the Form

You’ll need to use a templated event handler to show the form when the “New Contact” button is clicked. Add this event handler to the Create Control in contacts.js:

'{document} #new-contact click': function(){
  this.show();
}

The “New Contact” button is outside of the Create Control’s element, so '{document} #new-contact' is used as the selector for the button. When it is clicked, the form will slide down into view.


Initializing the Create Control

Just like the other Controls in you application, you’ll need to create a new instance of the Create Control. Update your document ready function in contacts.js to look like this:

$(document).ready(function(){
  $.when(Category.findAll(), Contact.findAll()).then(function(categoryResponse, contactResponse){
    var categories = categoryResponse[0], 
      contacts = contactResponse[0];

    new Create('#create', {
      categories: categories
    });
    new Contacts('#contacts', {
      contacts: contacts,
      categories: categories
    });
    new Filter('#filter', {
      contacts: contacts,
      categories: categories
    });
  });
})

With this change, an instance of the Create Control will be created on the #create element. It will be passed the list of categories.


Reacting to a New Contact

When a new contact is created, the Model.List stored in the Contacts Control needs to be updated. You do this using templated event handlers. Add this event handler to the Contacts Control in contacts.js:

'{Contact} created' : function(list, ev, contact){
  this.options.contacts.push(contact);
}

This binds to the created event of the Contact Model. The new contact is added to the Model.List stored in the Contacts Control using push().

Live binding will update your applications UI automatically when the contact is added to this.options.contacts.

Wrapping Up

That’s all for the final part of this tutorial. In part three you:

  • Used event handlers in a Control to create a new contact
  • Created a View that that render a create form
  • Used templated event handlers in a Control to bind to objects other than the Control’s element

This is the end of the CanJS contacts manager tutorial. Here’s a summary of what was covered in this three part tutorial:

  • Creating Controls to manage application logic
  • Render parts of an application with Views
  • Representing an application’s data layer using Models
  • Simulating a REST service with fixtures
  • Using live binding to keep an application’s UI in sync with its data layer
  • Listening to events with Control’s event handlers
  • Working with lists of model instances using Model.List

You now have everything you need to build JavaScript applications using CanJS. Go build something awesome.

For complete documentation and more example apps, visit CanJS. Thanks for reading!

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

    One question: what is the purpose of the weird dropdown for contact category?
    It’s not clearly displayed anywhere even when you select a value

    Thanks for the tut

  • http://www.pixelmauler.com DJ

    Thanks, this has been a great way to dive into JavaScript MVC!

  • kankaro

    Hope you can write a tuts about jQuery++ thanks a lot for this wonderful tuts :D

  • Jaap

    Are there any articles that compare different Javascript MVCs such as Angular, Can, Ember etc and the use case for using each of them?

    Main reason I ask is because I am a Javascript n00b and am working with MVC in general for the first time. I created a mini application using just jQuery and realised the need to use a MVC framework. I am learning using Angular at the moment but it seems kinda tough since there aren’t as many tutorials since it is still in beta. But I really like the fact that they make unit and E2E testing super easy.

  • Peder

    Thanks for the tutorials. Great intro to CanJS. It looks like an awesome framework, well done!

  • Oscar B.

    Hi. Great to see the final part of the series.
    I want to notice that in my box – Fedora 15 linux | Chrome 17 | Firefox 11 – the dropdown/select control is too narrow to display text.

    Anyway, great tutorial.

  • http://twitter.com/vladimir_light V-Light

    First of all, thanks for these great tutorials.

    It would be also great to see a REAL asynchronous requests. Although can.fixtures simulates RESTful-ish requests/responses, the returned data ist still static and the whole app isn’t realy asynchronous (be free to correct me, if i’m wrong)

    The point is – to demonstrate a real-life AJAX and how the App deals with it. (e.g. Failed PUT/POST request, where the behaviour of the app depends on a single request)

  • milo
    • Shane

      This is exactly what I was looking to do next. Thanks Milo.

  • tag

    nice work ….
    ………………..thanks.

  • http://modetrends.over-blog.com/ my blog

    It is definitely a fantastic and also beneficial part of data. Now i’m happy that you simply discussed this particular helpful data along with us. Remember to continue to be people up to date like this. Thank you for sharing.

  • http://wpbakery.com Michael

    Interesting read, thanks!

  • http://yckart.com/ Yannick Albert

    In Use with jQuery 1.8.0, it produces the following error:
    Uncaught TypeError: Cannot read property ’0′ of undefined [contacts.js:189]

    I’ve looked at the jQuery documentation but nothing found! Any Ideas?

  • Keith Loy

    I was encountering a problem with ‘{Contact} created’ : function(list, ev, contact){}) failing in the ContactView. I finally did a console.info(arguments) and discovered the arguments being passed to the callback had changed to function (ev, contact). Notice list is removed. I didn’t discover this until I rebuilt this app using requirejs, but it is something to be aware of.

  • Keith Loy

    I have setup a project on Github with this tutorial made using AMD. I did not use any globals in it.

    https://github.com/kloy/diving-into-canjs-amd

    • O Padilla

      Thanks for doing this