CanJS - Building A Contact List

canjs

How to render a list of contacts?

Templates can be loaded from a file or script tag. In this tutorial templates will be loaded from EJS files. To render contacts, you'll need an EJS template. Save the following code as contactsList.ejs within your views folder:

<ul class="clearfix">
  <% list(contacts, function(contact){ %>
    <li class="contact span8" <%= (el)-> el.data('contact', contact) %>>
      <%== can.view.render('views/contactView.ejs', {
        contact: contact, categories: categories
      }) %>
    </li>
  <% }) %>
</ul>

The EJS list() helper invokes a callback function on each contact in the list. When used with an observable list, the list() helper will use live binding to re-run anytime the length of the list changes.

<li class="contact span8" <%= (el)-> el.data('contact', contact) %>>

The code above uses an element callback to add the contact instance to the data of the <li>. Everything after the arrow is wrapped in a function that will be executed with el set to the current element.

<%== can.view.render('views/contactView.ejs', {
  contact: contact, categories: categories
}) %>

This code renders the contactView.ejs sub-template for each contact. can.view.render() takes a template and data as its parameters and returns HTML.

How to make your view live?

Any time EJS encounters attr() while processing a template, it knows that the surrounding code should be turned into an event handler bound to that property's changes. When the property is changed elsewhere in the app, the event handler is triggered and your UI will be updated. This is referred to as live binding. EJS Live binding is opt-in. It only turns on if you use attr() to access properties.

Let's look at one of the <input> tags from the contactView.ejs to see how this works:

<input type="text" name="name" placeholder="Add Name"
  <%= contact.attr('name') ? "value='" + contact.name + "'" : "class='empty'" %>>

The code in the magic tags will become an event handler bound to the contact's name property. When we update the name property, the event handler is run and the HTML will be updated.

What are the advantages of creating a control and binding it to a DOM element?

can.Control creates an organized, memory-leak free, stateful control that can be used to create widgets or organize application logic. You create an instance of a Control on a DOM element and pass it data your control will need. You can define any number of functions in your Control and bind to events. When the element your Control is bound to is removed from the DOM, the Control destroys itself, cleaning up any bound event handlers.

What are the important variables and functions present in every Control instance?

  • this - A reference to the Control instance
  • this.element - The DOM element that you created the instance on
  • this.options - An object containing any data passed to the instance when it was created
  • init() - Called when an instance is created

How to create a Control?

Contacts = can.Control({
  init: function(){
    this.element.html(can.view('views/contactsList.ejs', {
      contacts: this.options.contacts,
      categories: this.options.categories
    }));
  }
})

new Contacts('#contacts', {
  contacts: contacts,
  categories: categories
});

In the above code, we first define the class Contacts, and then we create an instance of the class.

When an instance of Contacts is created, init() will do two things:

  1. Uses can.view() to render contacts. can.view() accepts two parameters: the file or id of the script tag containing our template code and data. It returns the rendered result as a documentFragment (a lightweight container that can hold DOM elements).
  2. Inserts the documentFragment from can.view() into the Control's element using jQuery's .html().

The above code create an instance of the Contact Control on the #contacts element. The list of contacts and categories are passed into the Control.

How to define models and routes?

Contact = can.Model({
  findAll: 'GET /contacts',
  create  : "POST /contacts",
  update  : "PUT /contacts/{id}",
  destroy : "DELETE /contacts/{id}"
},{});

Category = can.Model({
  findAll: 'GET /categories'
},{});

What are the 5 static methods that we must define for each model?

A model has five static methods that you can define to create, retrieve, update and delete data. They are findAll, findOne, create, update and destroy. You can overwrite these functions to work with any back-end, but the easiest way to define a Model is using REST service, as exemplified in the code above. You can safely omit any static methods that won't be used in an application.

What is the relationship between can.Observe and can.Model?

Model instances in CanJS are actually 'observables'. can.Observe provides the observable pattern for objects and can.Observe.List provides the observable pattern for arrays. This means you can get and set properties using attr() and bind to changes in those properties.

What does the findAll method return?

The findAll() method returns a Model.list, which is a can.Observe.List that triggers events when an element is added or removed from the list.

What is a fixture?

Fixtures intercept AJAX requests and simulate their response with a file or function. This is fantastic for testing, prototyping or when a back-end isn't ready yet. Fixtures are needed to simulate the REST service the models in this application are using.

var CONTACTS = [
  {
    id: 1,
    name: 'William',
    address: '1 CanJS Way',
    email: 'william@husker.com',
    phone: '0123456789',
    category: 'co-workers'
  },
  {
    id: 2,
    name: 'Laura',
    address: '1 CanJS Way',
    email: 'laura@starbuck.com',
    phone: '0123456789',
    category: 'friends'
  },
  {
    id: 3,
    name: 'Lee',
    address: '1 CanJS Way',
    email: 'lee@apollo.com',
    phone: '0123456789',
    category: 'family'
  }
];

var CATEGORIES = [
  {
    id: 1,
    name: 'Family',
    data: 'family'
  },
  {
    id: 2,
    name: 'Friends',
    data: 'friends'
  },
  {
    id: 3,
    name: 'Co-workers',
    data: 'co-workers'
  }
];

Now that you have some data, you need to wire it up to fixtures so you can simulate a REST service.

can.fixture() takes two parameters. The first is the URL we want to intercept and the second is a file or function that is used to generate a response. Often URLs you want to intercept are dynamic and follow a pattern. In this case, you should use templated URLs. Simply add curly braces to the URL where you want to match wildcards.

can.fixture('GET /contacts', function(){
  return [CONTACTS];
});

var id= 4;
can.fixture("POST /contacts", function(){
  return {id: (id++)}
});

can.fixture("PUT /contacts/{id}", function(){
  return {};
});

can.fixture("DELETE /contacts/{id}", function(){
  return {};
});

can.fixture('GET /categories', function(){
  return [CATEGORIES];
});

Now you can use the fixture:

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

      new Contacts('#contacts', {
        contacts: contacts,
        categories: categories
      });
  });
});

The above code use deferred. The findAll methods from the Category and Contact models are use to load the data. The results are passed onto the function as categoryResponse and contactResponse, and then we create the instance of the Contacts control which will render the data (see the definition of the Contacts control defined above).

What is Model.List?

A Model.List is an observable array of model instances. When you define a Model like Contact, a Model.List for that type of Model is automatically created.

Can we extend Model.List?

Yes. A Model.List is an observable array of model instances. When you define a Model like Contact, a Model.List for that type of Model is automatically created. We can extend this created Model.List to add helper functions that operate on a list of model instances.

Contact.List will need two helper functions to filter a list of contacts and report how many contacts are in each category. Add this to contacts.js immediately after the Contact model:

Contact.List = can.Model.List({
  filter: function(category){
    this.attr('length');
    var contacts = new Contact.List([]);
    this.each(function(contact, i){
      if(category === 'all' || category === contact.attr('category')) {
        contacts.push(contact)
      }
    })
    return contacts;
  },
  count: function(category) {
    return this.filter(category).length;
  }
});

The two helper functions here are:

  • filter() loops through each contact in the list and returns a new Contact.List of contacts within a category. this.attr('length') is included here so EJS will setup live binding when we use this helper in a view.
  • count() returns the number of contacts in a category using the filter() helper function. Because of this.attr('length') in filter(), EJS will setup live binding when we use this helper in a view.

How to filter contacts?

In the contactsList.ejs view, change the parameter passed to the list() helper to contacts.filter(can.route.attr('category')). Your EJS file should look like this when you're done:

<ul class="unstyled clearfix">
  <% list(contacts.filter(can.route.attr('category')), function(contact){ %>
    <li class="contact span8" <%= (el)-> el.data('contact', contact) %>>
      <div class="">
        <%== can.view.render('contactView', {contact: contact, categories: categories}) %>
      </div>
    </li>
  <% }) %>
</ul>

On line two, filter() is called with the current category from can.route. Since you used attr() in filter() and on can.route, EJS will setup live binding to re-render your UI when either of these change.

How to display the list of categories?

<ul class="nav nav-list">
  <li class="nav-header">Categories</li>
  <li>
    <a href="javascript://" data-category="all">All (<%= contacts.count('all') %>)</a>
  </li>
  <% $.each(categories, function(i, category){ %>
    <li>
      <a href="javascript://" data-category="<%= category.data %>"><%= category.name %> (<%= contacts.count(category.data) %>)</a>
    </li>
  <% }) %>
</ul>

Each link has a data-category attribute that will be pulled into jQuery's data object. Later, this value can be accessed using .data('category') on the <a> tag. The category's name and number of contacts will be used as the link text. Live binding is setup on the number of contacts because count() calls filter() which contains this.attr('length').

How to listen to events with can.Control?

Control automatically binds methods that look like event handlers when an instance is created. The first part of the event handler is the selector and the second part is the event you want to listen to. The selector can be any valid CSS selector and the event can be any DOM event or custom event. So a function like 'a click' will listen to a click on any <a> tag within the control's element.

Control uses event delegation, so you don't have to worry about rebinding event handlers when the DOM changes.

Filter = can.Control({
  init: function(){
    var category = can.route.attr('category') || "all";
    this.element.html(can.view('filterView', {
      contacts: this.options.contacts,
      categories: this.options.categories
    }));
    this.element.find('[data-category="' + category + '"]').parent().addClass('active');
  },
  '[data-category] click': function(el, ev) {
    this.element.find('[data-category]').parent().removeClass('active');
    el.parent().addClass('active');
    can.route.attr('category', el.data('category'));
  }
});

The above code defines the Filter Control but does not create an instance. init() uses can.view() to render categories and html() to insert it in to the Control's element.

this.element.find('[data-category="' + category + '"]').parent().addClass('active');

The above code finds the link that corresponds to the current category and adds a class of 'active' to its parent element.

'[data-category] click': function(el, ev) {

listens for a click event on any element matching the selector [data-category].

this.element.find('[data-category]').parent().removeClass('active');
el.parent().addClass('active');

Removes the 'active' class from all links then adds a class of 'active' to the link that was clicked.

can.route.attr('category', el.data('category'));

updates the category property in can.route using the value from jQuery's data object for the <a> that was clicked.

How to update a contact? How to implement the U portion of CRUD?

Add this code 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();
}

The above code define 3 event handlers for focusout, keyup, and change. The above code also define the updateContact method, which is invoked by the 3 event handlers.

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.

How to delete a contact? How to implement the D portion of CRUD?

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. And live binding will automatically update your UI when a contact is deleted.

How to create control and view needed to create a contact? How to implement the C portion of CRUD?

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:

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();
    }
  }
});

In the show function, we 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.

We also define the createContact function and various event handlers that would calls this createContact function. Inside the createContact function, 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().

What is templated event handler?

Controls also support templated event handlers that allow you to customize an event handler and listen to events on objects other than the element that this control was bound to (namely this.element).

How to customize an event handler to listen to events on objects other than the element that this control was bound to?

When we create a control, we can bind it to a DOM element. When the element is removed from the DOM, the control is automatically destroyed and removed from memory. We can use "templated event handler" to listen to events on objects other than the element that this control was bound to. To do this, we use the {NAME} construct 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.

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

The above code should be added to the Create Control. It is used to display the form when the "New Contact" button is clicked.

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.

How to added the newly created contact to the Model.List?

Perhaps, CanJS should automatically add the newly created contact to the Model.List, but this is currently not being done automatically. 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:

'{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(). In the above code, this.options.contacts is the result of the findAll method that was invoked earlier.

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

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License