This is part two of a three part series 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 one, you created the Models, Views and Controls needed to display contacts and used fixtures to simulate a REST service.
In this part, you will:
- Create a Control and View to display categories.
- Listen to events using a Control.
- Use routing to filter contacts.
You'll be adding to the source files from part one, so if you haven't done so already, go catch up first. I'll be here when you're ready.
Setting Up Routing
Routing helps manage browser history and client state in single page JavaScript applications.
Routing helps manage browser history and client state in single page JavaScript applications. The hash in the URL contains properties that an application reads and writes. Various parts of the app can listen to these changes and react accordingly, usually updating parts of the current page without loading a new one.
can.route
is a special observable that updates and responds to changes in window.location.hash
. Use can.route
to map URLs to properties, resulting in pretty URLs like #!filter/all
. If no routes are defined, the hash value is just serialized into URL encoded notation like #!category=all
.
In this application, routing will be used to filter contacts by category. Add the following code to your contacts.js
file:
can.route( 'filter/:category' ) can.route('', {category: 'all' })
The first line creates a route with a category
property that your application will be able to read and write. The second line creates a default route, that sets the category
property to all
.
Working With a List Of Model Instances
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 newContact.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 thefilter()
helper function. Because ofthis.attr('length')
infilter()
, EJS will setup live binding when we use this helper in a view.
If you'll be using a helper in EJS, use
attr()
on a list or instance property to setup live binding.
Filtering Contacts
Next, you'll modify the contactsList.ejs
view to filter contacts based on the category property in the hash. 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.
By now it should be clear how powerful live binding is. With a slight tweak to your view, the UI of the app will now be completely in sync with not only the list of contacts, but with the category property defined in the route as well.
Displaying Categories
Contacts are filtered when the category property in the hash is changed. Now you need a way to list all available categories and change the hash.
First, create a new View to display a list of categories. Save this code as filterView.ejs
in your views
folder:
<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>
Let's go over a few lines from this code and see what they do:
<% $.each(categories, function(i, category){ %>
$.each
loops through the categories and executes a callback for each one.
<a href="javascript://" data-category="<%= category.data %>"><%= category.name %> (<%= contacts.count(category.data) %>
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 test. Live binding is setup on the number of contacts because count()
calls filter()
which contains this.attr('length')
.
Listening 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.
Displaying Categories
Create the Control that will manage categories by adding this code to contacts.js
right after the Contacts
Control:
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')); } });
Let's examine the code from the `Filter` Control you just created:
this.element.html(can.view('filterView', { contacts: this.options.contacts, categories: this.options.categories }));
Like in the Contacts
Control, 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');
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.
Initializing the Filter Control
Just like the Contacts
Control in part one, you need to create a new instance of the Filter
Control. Update your document ready function to look like this:
$(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 }); new Filter('#filter', { contacts: contacts, categories: categories }); }); })
With this change, an instance of the Filter
Control will be created on the #filter
element. It will be passed the list of contacts and categories.
Now, when you run your application in a browser, you will be able to filter contacts by clicking on the categories on the right:
Wrapping Up
That's all for part two! Here's what we've accomplished:
- Created a Control that listens to events and manages categories
- Setup routing to filter contacts by category
- Tweaked your views so live binding will keep your entire UI in sync with your data layer
In part three, you'll update your existing Controls to allow contacts to be edited and deleted. You'll also create a new Control and View to that enables you to add new contacts.
Can't wait to learn more? Part three of the series has been posted here!
Comments