Notice: Undefined index: order_next_posts in /nas/content/live/gadgetmag/wp-content/plugins/smart-scroll-posts/smart-scroll-posts.php on line 194

Notice: Undefined index: post_link_target in /nas/content/live/gadgetmag/wp-content/plugins/smart-scroll-posts/smart-scroll-posts.php on line 195

Notice: Undefined index: posts_featured_size in /nas/content/live/gadgetmag/wp-content/plugins/smart-scroll-posts/smart-scroll-posts.php on line 196

Get better code management with Backbone.js

Join us as we show you how Backbone.js can provide structure for your unmanageable spaghetti code


Create the HTML

We begin by creating the basic markup for our task list. This will serve to be the container for all new tasks. While we’re here, we should also create two buttons for adding and clearing all completed tasks. Finally, we’ll add an empty unordered list, which will house the tasks.

001 <h1>My Todos</h1>
 002 <div id="tasks">
 003     <button>Add Task</button>
 004     <button>Clear All</button>
 005     <ul></ul>
 006 </div>

The to-do model

Next, we need to focus on the Task model, or blueprint for each task: The model is the core of any application, and is where we can store defaults, as well as perform any necessary validation. In Backbone, creating a new model is as easy as extending Backbone.Model.

001 var Task = Backbone.Model.extend({
 002    defaults: { text: 'New task', completed: false } });

The TasksView view

If a model is responsible for the data, then a view is responsible for the presentation of that data. In Backbone, however, a view is a bit different from what you might have previously experienced using a server-side language. A view should ideally represent a single DOM element. For a task, we’ll use a list item as its container.

001 var TasksView = Backbone.View.extend({
 002    tagName: 'li',
 003    initialize: function() {} });

Create the task template

Think about it: for each new task, we need to have some way to edit, delete, and complete it. While we could store a long string of HTML in our JavaScript, doing so is generally considered by most developers to be bad practice. Instead, let’s create a template and take advantage of Underscore’s built-in templating abilities.

001 <script id="taskTemplate" type="text/template">
 002    <span><%= text %></span>
 003    <button>✓</button>
 004    <button>X</button>
 005 </script>

Apply the template

With a template now in place, let’s run it through Underscore’s template method. Next, within the render method, which is responsible for generating the structure of the view’s element, we merge the template with the associated model’s data, and append the generated HTML to the view’s root element: the list item.

001 template: _.template( $('#taskTemplate').html() ),
 002 initialize: function() { this.render(); },
 003 render: function() {
 004     var markup = this.template(this.model.toJSON());
 005     this.$el.html(markup);
 006 }        

Tasks wrapper view

So far, we have a view for a specific task item. However, we need a new view – one that will represent the tasks container. In step 1, we created the #tasks wrapper div, along with two buttons to add and clear completed tasks. So, let’s bind our new view to that element in the DOM.

001 var TasksView = Backbone.View.extend({
 002    el: '#tasks'
 003 });

Event listeners

Now that we have a view for the tasks wrapper, let’s listen out for when the Add Task button is clicked. In Backbone, attaching event listeners is a complete cinch. When this button is clicked, we’ll use a simple prompt to query the user for their desired task, and then create a new model with that data.

001 events: { 'click .add': 'add' },
 002 add: function() {
 003 {
 004 var text = prompt('What do you need to do?');
 005 var task = new Task({ text: text });
 006 }

A collection of tasks

At this point, you might be wondering where we’re going to store all of these tasks. In the last step, we successfully created a new Task model instance, but what happens when we have ten new tasks? We need an easy way to store them. The solution is to use Backbone collections, which you can think of as glorified arrays.

001 var Tasks = Backbone.Collection.extend({
 002 model: Task
 003 });

Adding to the collection

Now we have a special container for our tasks. If it helps, for now, think of a collection as an array with some added sugar. Within the TasksView’s initialize method, which automatically runs when a new instance is created, let’s create a new collection instance variable. Next, back to the add method, we add the new task to the tasks collection.

001 el: '#tasks',
 002 initialize: function() { this.collection = new Tasks; },
 003 // ...
 004 add: function() {
 005 var text = prompt('What do you need to do?');
 006 var task = new Task({ text: text });
 007 this.collection.add(task);
 008 }

Collection events

The add method is responsible for creating a new instance of the Task model, and adding it to the associated collection. But, we also need a way to update the DOM as well. Luckily, models and collections will make announcements when they are modified. Let’s listen for a newly added item, and then update the DOM, accordingly.

001 initialize: function() {
 002 this.collection = new Tasks;
 003 this.collection.on('add', this.appendNewTask, this);
 004 },
 005 // ...
 006 appendNewTask: function(task) {
 007 var TasksView = new TasksView({ model: task });
 008 }

Appending the content

Remember: within TasksView, the root element is actually a div. But we need to append new tasks to the unordered list that is a child of that div. So let’s add a new property to the view, items, which points to the unordered list, and then append the new task directly to it.

001 initialize: function() {
 002 // ...
 003 this.items = this.$el.children('ul');
 004 },
 005 // ...
 006 appendNewTask: function(task) {
 007 var TasksView = new TasksView({ model: task });
 008 this.items.append(TasksView.el);
 009 }

Deleting a task

So far, so good. We can now add any number of new tasks, and they will correctly be thrown into the DOM accordingly The next step is for us to listen for when the user clicks on the Delete button. When they do, we need to destroy the associated model and remove the element from the DOM. Luckily, that’s pretty easy.

001 var TasksView = Backbone.View.extend({
 002 tagName: 'li',
 003 events: {
 004 'click .delete': 'delete'
 005 },
 006 // ...
 007 delete: function() {
 008 this.model.destroy();
 009 this.$el.remove();
 010 }
 011 });

Completing a task

It wouldn’t be much of a to-do list if the user doesn’t have the ability to mark a task as being complete. To implement this functionality, we’ll need to listen for when the Completed (the checkmark) button is clicked. When it is, we will update the model, and toggle its completed property.

001 {
 002 'click .delete': 'delete',
 003 'click .complete': 'updateStatus'
 004 },
 005 // ...
 006 updateStatus: function() {
 007 this.model.set('completed', !this.model.get('completed'));
 008 // ...

Set the completed class

In the previous step, we only updated the model’s completed property. This is helpful to us, but it isn’t yet reflected on the actual page. Let’s update the task template to apply a class, according to whether the completed property on the model is set to true or false. This way, we can apply the necessary CSS to create the visual of a completed task. text-decoration: line-through will do the trick nicely.

001< script id="taskTemplate" type="text/javascript">// < ![CDATA[
 // <  ![CDATA[
 002 < %= text %>
 005 // ]]>< /script>

Apply the CSS

At this point, each time the user clicks the checkmark button, the element’s class name will toggle from incomplete to complete. This is exactly what we need. Now, we apply the necessary CSS so that, only when a task has been marked as complete, will it be presented as so.

Edit a task

Though not paramount, it would be nice if we offered some way for the user to edit the text of a task. Let’s implement the necessary functionality so that when the user double-clicks on a task, a new prompt is revealed, which allows the user to update their task.

001 events: {
 002 'click .delete': 'delete',
 003 'click .complete': 'updateStatus',
 004 'dblclick span': 'edit'
 005 },
 006 // …
 007 edit: function() {
 008 var text = prompt('What should we change your task to?', this.model.get('text') );
 009 this.model.set('text', text);
 010 },

Model events

In its current state, if we edit a task, it does in fact update the model with the new text; however, the view remains the same. To fix this, we’ll subscribe to when the model changes. When it does, we can re-render the view. It really couldn’t be simpler.

001 initialize: function() { 
 002 this.model.on('change', this.render, this);
 003 this.render();
 004 }

Clear completed tasks

For our final trick, we’ll allow the user to clear all completed tasks with the click of a button. The first step in making this a reality is to add a new method to the Tasks collection, completed, which will return only the models, where the completed attribute has been set to true.

001 var Tasks = Backbone.Collection.extend({
 002 model: Task,
 004 completed: function() {
 005 return _.filter(this.models, function(model) {
 006 return model.get('completed');
 007 });
 008 }
 009 });

Attach the listener

The next step is to listen for when the user clicks the Clear Completed button from within the TasksView view. When they do, we’ll fetch all the completed tasks, and remove them from the collection. Take care to note, however, that doing so won’t yet update the DOM. We’ll take care of that business in the following step.

001 events: {
 002 'click .add': 'add',
 003 'click .clear': 'clearCompleted'
 004 },
 005 // ...
 006 clearCompleted: function() {
 007 var completedTasks = this.collection.completed();
 008 this.collection.remove(completedTasks);
 009 }

Remove the root element

The final step is for the TasksView instance(s) to listen for when the associated model has been removed. When it is, we call the unrender method, which will remove the root element entirely from the DOM. Depending on how many tasks were marked as complete, this method can potentially run multiple times.

001 initialize: function() { 
 002 this.model.on('remove', this.unrender, this);
 003 // ...
 004 },
 005 // ...
 006 unrender: function() {
 007 this.$el.remove();
 008 }

Here we go

The structure is in place: the model, views, and collection. But, if you view the page in your browser, nothing will happen! To get the ball rolling, we only need to create a new instance of TasksView, and instantly, our task list tool will be up and running.

001 // Here we go!!
 002 new TasksView;

Test file

With relatively little effort, we now have the ability to create, edit, delete, and complete any number of tasks. But, more importantly, our code is beautifully structured into small components. To take things further, consider using RequireJS to organise your application. Remember: the key to building large JavaScript applications is… Not building large JavaScript applications!