Now that weâve covered fundamentals, letâs write our first Backbone.js application. Weâll build the Backbone Todo List application exhibited on TodoMVC.com. Building a todo list is a great way to learn Backboneâs conventions (see Figure 4-1). Itâs a relatively simple application, yet technical challenges surrounding binding, persisting model data, routing, and template rendering provide opportunities to illustrate some core Backbone features.
Letâs consider the applicationâs architecture at a high level. Weâll need:
A Todo model to describe individual todo items
A
TodoList
collection to store and persist todosA way of creating todos
A way to display a listing of todos
A way to edit existing todos
A way to mark a todo as completed
A way to delete todos
A way to filter the items that have been completed or are remaining
Essentially, these features are classic CRUD (create, read, update, delete) methods. Letâs get started!
Weâll place all of our HTML in a single file named index.html.
First, weâll set up the header and the basic application dependencies: jQuery, Underscore, Backbone.js, and the Backbone localStorage adapter.
<!doctype html>
<html
lang=
"en"
>
<head>
<meta
charset=
"utf-8"
>
<meta
http-equiv=
"X-UA-Compatible"
content=
"IE=edge,chrome=1"
>
<title>
Backbone.js ⢠TodoMVC</title>
<link
rel=
"stylesheet"
href=
"assets/base.css"
>
</head>
<body>
<script
type=
"text/template"
id=
"item-template"
></script>
<script
type=
"text/template"
id=
"stats-template"
></script>
<script
src=
"js/lib/jquery.min.js"
></script>
<script
src=
"js/lib/underscore-min.js"
></script>
<script
src=
"js/lib/backbone-min.js"
></script>
<script
src=
"js/lib/backbone.localStorage.js"
></script>
<script
src=
"js/models/todo.js"
></script>
<script
src=
"js/collections/todos.js"
></script>
<script
src=
"js/views/todos.js"
></script>
<script
src=
"js/views/app.js"
></script>
<script
src=
"js/routers/router.js"
></script>
<script
src=
"js/app.js"
></script>
</body>
</html>
In addition to the aforementioned dependencies, note that a few other application-specific files are also loaded. These are organized into folders representing their application responsibilities: models, views, collections, and routers. An app.js file houses central initialization code.
If you want to follow along, create a directory structure as demonstrated in index.html:
Place the index.html in a top-level directory.
Download jQuery, Underscore, Backbone, and Backbone localStorage from their respective websites and place them under js/lib.
Create the directories js/models, js/collections, js/views, and js/routers.
You will also need base.css and bg.png, which should live in an assets directory. And remember that you can see a demo of the final application at TodoMVC.com.
We will be creating the application JavaScript files during the
tutorial. Donât worry about the two text/template
script
elementsâwe will replace those soon!
Now letâs populate the body of index.html.
Weâll need an <input>
for
creating new todos, a <ul id="todo-list"
/>
for listing the actual todos, and a footer where we can
later insert statistics and links for performing operations such as
clearing completed todos. Weâll add the following markup immediately
inside our <body>
tag before the
script
elements:
<section
id=
"todoapp"
>
<header
id=
"header"
>
<h1>
todos</h1>
<input
id=
"new-todo"
placeholder=
"What needs to be done?"
autofocus
>
</header>
<section
id=
"main"
>
<input
id=
"toggle-all"
type=
"checkbox"
>
<label
for=
"toggle-all"
>
Mark all as complete</label>
<ul
id=
"todo-list"
></ul>
</section>
<footer
id=
"footer"
></footer>
</section>
<div
id=
"info"
>
<p>
Double-click to edit a todo</p>
<p>
Written by<a
href=
"https://github.com/addyosmani"
>
Addy Osmani</a></p>
<p>
Part of<a
href=
"http://todomvc.com"
>
TodoMVC</a></p>
</div>
To complete index.html, we need to add the
templates, which we will use to dynamically create HTML by injecting
model data into their placeholders. One way of including templates in
the page is by using custom <script>
tags.
These donât get evaluated by the browser, which just interprets them as
plain text. Underscore microtemplating can then access the templates,
rendering fragments of HTML.
Weâll start by filling in the #item-template
,
which will be used to display individual todo items.
<!-- index.html -->
<script
type=
"text/template"
id=
"item-template"
>
<
div
class
=
"view"
>
<
input
class
=
"toggle"
type
=
"checkbox"
<%=
completed
?
'checked'
:
''
%>>
<
label
><%-
title
%><
/label>
<
button
class
=
"destroy"
><
/button>
<
/div>
<
input
class
=
"edit"
value
=
"<%- title %>"
>
</script>
The template tags in the preceding markup, such as <%=
and <%-
, are specific to Underscore.js and are documented on the
Underscore site. In your own applications, you have a choice of template
libraries, such as Mustache or Handlebars. Use whichever you prefer;
Backbone doesnât mind.
We also need to define the #stats-template
,
which we will use to populate the footer.
<!-- index.html -->
<script
type=
"text/template"
id=
"stats-template"
>
<
span
id
=
"todo-count"
><
strong
><%=
remaining
%><
/strong>
<%=
remaining
===
1
?
'item'
:
'items'
%>
left
<
/span>
<
ul
id
=
"filters"
>
<
li
>
<
a
class
=
"selected"
href
=
"#/"
>
All
<
/a>
<
/li>
<
li
>
<
a
href
=
"#/active"
>
Active
<
/a>
<
/li>
<
li
>
<
a
href
=
"#/completed"
>
Completed
<
/a>
<
/li>
<
/ul>
<%
if
(
completed
)
{
%>
<
button
id
=
"clear-completed"
>
Clear
completed
(
<%=
completed
%>
)
<
/button>
<%
}
%>
</script>
The #stats-template
displays the number of
remaining incomplete items and contains a list of hyperlinks that will
be used to perform actions when we implement our router. It also
contains a button that can be used to clear all of the completed
items.
Now that we have all the HTML that we will need, weâll start implementing our application by returning to the fundamentals: a Todo model.
The Todo model is remarkably straightforward. First, a todo has two
attributes: a title
stores a todo itemâs title, and a
completed
status indicates whether itâs
complete. These attributes are passed as defaults, as shown
here:
// js/models/todo.js
var
app
=
app
||
{};
// Todo Model
// ----------
// Our basic **Todo** model has 'title', 'order', and 'completed' attributes.
app
.
Todo
=
Backbone
.
Model
.
extend
({
// Default attributes ensure that each todo created has `title` and
// `completed` keys.
defaults
:
{
title
:
''
,
completed
:
false
},
// Toggle the `completed` state of this todo item.
toggle
:
function
()
{
this
.
save
({
completed
:
!
this
.
get
(
'completed'
)
});
}
});
Second, the Todo model has a toggle()
method through which a todo itemâs
completion status can be set and simultaneously persisted.
Next, a TodoList
collection is
used to group our models. The collection uses the localStorage adapter to override Backboneâs
default sync()
operation with one that
will persist our todo records to HTML5 localStorage. Through localStorage,
theyâre saved between page requests.
// js/collections/todos.js
var
app
=
app
||
{};
// Todo Collection
// ---------------
// The collection of todos is backed by *localStorage* instead of a remote
// server.
var
TodoList
=
Backbone
.
Collection
.
extend
({
// Reference to this collection's model.
model
:
app
.
Todo
,
// Save all of the todo items under the `"todos-backbone"` namespace.
// Note that you will need to have the Backbone localStorage plug-in
// loaded inside your page in order for this to work. If testing
// in the console without this present, comment out the next line
// to avoid running into an exception.
localStorage
:
new
Backbone
.
LocalStorage
(
'todos-backbone'
),
// Filter down the list of all todo items that are finished.
completed
:
function
()
{
return
this
.
filter
(
function
(
todo
)
{
return
todo
.
get
(
'completed'
);
});
},
// Filter down the list to only todo items that are still not finished.
remaining
:
function
()
{
// apply allowsus to define the context of this within our function scope
return
this
.
without
.
apply
(
this
,
this
.
completed
()
);
},
// We keep the Todos in sequential order, despite being saved by unordered
// GUID in the database. This generates the next order number for new items.
nextOrder
:
function
()
{
if
(
!
this
.
length
)
{
return
1
;
}
return
this
.
last
().
get
(
'order'
)
+
1
;
},
// Todos are sorted by their original insertion order.
comparator
:
function
(
todo
)
{
return
todo
.
get
(
'order'
);
}
});
// Create our global collection of **Todos**.
app
.
Todos
=
new
TodoList
();
The collectionâs completed()
and
remaining()
methods return an array of
finished and unfinished todos, respectively.
A nextOrder()
method implements a
sequence generator while a comparator()
sorts items by their insertion order.
Note
this.filter
, this.without
, and this.last
are Underscore methods that are
mixed in to Backbone.Collection
so that the reader
knows how to find out more about them.
Letâs examine the core application logic that resides in the views. Each view supports functionality such as edit-in-place, and therefore contains a fair amount of logic. To help organize this logic, weâll use the element controller pattern. The element controller pattern consists of two views: one controls a collection of items, while the other deals with each individual item.
In our case, an AppView
will
handle the creation of new todos and rendering of the initial todo list.
Instances of TodoView
will be
associated with each individual todo record. Todo instances can handle
editing, updating, and destroying their associated todo.
To keep things short and simple, we wonât be implementing all of the
applicationâs features in this tutorial; weâll just cover enough to get
you started. Even so, there is a lot for us to cover in AppView
, so weâll split our discussion into two
sections.
// js/views/app.js
var
app
=
app
||
{};
// The Application
// ---------------
// Our overall **AppView** is the top-level piece of UI.
app
.
AppView
=
Backbone
.
View
.
extend
({
// Instead of generating a new element, bind to the existing skeleton of
// the app already present in the HTML.
el
:
'#todoapp'
,
// Our template for the line of statistics at the bottom of the app.
statsTemplate
:
_
.
template
(
$
(
'#stats-template'
).
html
()
),
// At initialization we bind to the relevant events on the `Todos`
// collection, when items are added or changed.
initialize
:
function
()
{
this
.
allCheckbox
=
this
.
$
(
'#toggle-all'
)[
0
];
this
.
$input
=
this
.
$
(
'#new-todo'
);
this
.
$footer
=
this
.
$
(
'#footer'
);
this
.
$main
=
this
.
$
(
'#main'
);
this
.
listenTo
(
app
.
Todos
,
'add'
,
this
.
addOne
);
this
.
listenTo
(
app
.
Todos
,
'reset'
,
this
.
addAll
);
},
// Add a single todo item to the list by creating a view for it, and
// appending its element to the `<ul>`.
addOne
:
function
(
todo
)
{
var
view
=
new
app
.
TodoView
({
model
:
todo
});
$
(
'#todo-list'
).
append
(
view
.
render
().
el
);
},
// Add all items in the **Todos** collection at once.
addAll
:
function
()
{
this
.
$
(
'#todo-list'
).
html
(
''
);
app
.
Todos
.
each
(
this
.
addOne
,
this
);
}
});
There are a few notable features in our initial version of
AppView
, including a statsTemplate
, an initialize
method thatâs implicitly called on
instantiation, and several view-specific methods.
An el
(element) property stores a
selector targeting the DOM element with an id
of
todoapp
. In the case of our
application, el
refers to the matching
<section id="todoapp" />
element
in index.html.
The call to _.template
uses Underscoreâs
microtemplating to construct a statsTemplate
object
from our #stats-template
. We will use this template
later when we render our view.
Now letâs take a look at the initialize
function. First, itâs using jQuery to
cache the elements it will be using into local properties (recall that
this.$()
finds elements relative to
this.$el
). Then itâs binding to two
events on the Todos collection: add
and
reset
. Since weâre delegating handling
of updates and deletes to the TodoView
view, we donât need to worry about those here. The two pieces of logic
are:
When an
add
event is fired, theaddOne()
method is called and passed the new model.addOne()
creates an instance of theTodoView
view, renders it, and appends the resulting element to our todo list.When a
reset
event occurs (we update the collection in bulk as happens when the todos are loaded from localStorage),addAll()
is called and iterates over all of the todos currently in our collection, firingaddOne()
for each item.
Note that we were able to use this
within addAll()
to refer to the view because listenTo()
implicitly set the callbackâs context
to the view when it created the binding.
Now, letâs add some more logic to complete our
AppView
:
// js/views/app.js
var
app
=
app
||
{};
// The Application
// ---------------
// Our overall **AppView** is the top-level piece of UI.
app
.
AppView
=
Backbone
.
View
.
extend
({
// Instead of generating a new element, bind to the existing skeleton of
// the app already present in the HTML.
el
:
'#todoapp'
,
// Our template for the line of statistics at the bottom of the app.
statsTemplate
:
_
.
template
(
$
(
'#stats-template'
).
html
()
),
// New
// Delegated events for creating new items, and clearing completed ones.
events
:
{
'keypress #new-todo'
:
'createOnEnter'
,
'click #clear-completed'
:
'clearCompleted'
,
'click #toggle-all'
:
'toggleAllComplete'
},
// At initialization we bind to the relevant events on the `Todos`
// collection, when items are added or changed. Kick things off by
// loading any preexisting todos that might be saved in *localStorage*.
initialize
:
function
()
{
this
.
allCheckbox
=
this
.
$
(
'#toggle-all'
)[
0
];
this
.
$input
=
this
.
$
(
'#new-todo'
);
this
.
$footer
=
this
.
$
(
'#footer'
);
this
.
$main
=
this
.
$
(
'#main'
);
this
.
listenTo
(
app
.
Todos
,
'add'
,
this
.
addOne
);
this
.
listenTo
(
app
.
Todos
,
'reset'
,
this
.
addAll
);
// New
this
.
listenTo
(
app
.
Todos
,
'change:completed'
,
this
.
filterOne
);
this
.
listenTo
(
app
.
Todos
,
'filter'
,
this
.
filterAll
);
this
.
listenTo
(
app
.
Todos
,
'all'
,
this
.
render
);
app
.
Todos
.
fetch
();
},
// New
// Rerendering the app just means refreshing the statistics -- the rest
// of the app doesn't change.
render
:
function
()
{
var
completed
=
app
.
Todos
.
completed
().
length
;
var
remaining
=
app
.
Todos
.
remaining
().
length
;
if
(
app
.
Todos
.
length
)
{
this
.
$main
.
show
();
this
.
$footer
.
show
();
this
.
$footer
.
html
(
this
.
statsTemplate
({
completed
:
completed
,
remaining
:
remaining
}));
this
.
$
(
'#filters li a'
)
.
removeClass
(
'selected'
)
.
filter
(
'[href="#/'
+
(
app
.
TodoFilter
||
''
)
+
'"]'
)
.
addClass
(
'selected'
);
}
else
{
this
.
$main
.
hide
();
this
.
$footer
.
hide
();
}
this
.
allCheckbox
.
checked
=
!
remaining
;
},
// Add a single todo item to the list by creating a view for it, and
// appending its element to the `<ul>`.
addOne
:
function
(
todo
)
{
var
view
=
new
app
.
TodoView
({
model
:
todo
});
$
(
'#todo-list'
).
append
(
view
.
render
().
el
);
},
// Add all items in the **Todos** collection at once.
addAll
:
function
()
{
this
.
$
(
'#todo-list'
).
html
(
''
);
app
.
Todos
.
each
(
this
.
addOne
,
this
);
},
// New
filterOne
:
function
(
todo
)
{
todo
.
trigger
(
'visible'
);
},
// New
filterAll
:
function
()
{
app
.
Todos
.
each
(
this
.
filterOne
,
this
);
},
// New
// Generate the attributes for a new todo item.
newAttributes
:
function
()
{
return
{
title
:
this
.
$input
.
val
().
trim
(),
order
:
app
.
Todos
.
nextOrder
(),
completed
:
false
};
},
// New
// If you hit return in the main input field, create new Todo model,
// persisting it to localStorage.
createOnEnter
:
function
(
event
)
{
if
(
event
.
which
!==
ENTER_KEY
||
!
this
.
$input
.
val
().
trim
()
)
{
return
;
}
app
.
Todos
.
create
(
this
.
newAttributes
()
);
this
.
$input
.
val
(
''
);
},
// New
// Clear all completed todo items, destroying their models.
clearCompleted
:
function
()
{
_
.
invoke
(
app
.
Todos
.
completed
(),
'destroy'
);
return
false
;
},
// New
toggleAllComplete
:
function
()
{
var
completed
=
this
.
allCheckbox
.
checked
;
app
.
Todos
.
each
(
function
(
todo
)
{
todo
.
save
({
'completed'
:
completed
});
});
}
});
We have added the logic for creating new todos, editing them, and filtering them based on their completed status.
Weâve defined an events
hash
containing declarative callbacks for our DOM events. It binds those events
to the following methods:
createOnEnter()
Creates a new Todo model and persists it in localStorage when a user presses Enter inside the
<input/>
field. Also resets the main<input/>
field value to prepare it for the next entry. The model is populated bynewAttributes()
, which returns an object literal composed of the title, order, and completed state of the new item. Note thatthis
is referring to the view and not the DOM element since the callback was bound using theevents
hash.clearCompleted()
Removes the items in the todo list that have been marked as completed when the user clicks the clear-completed checkbox (this checkbox will be in the footer populated by the
#stats-template
).toggleAllComplete()
Allows a user to mark all of the items in the todo list as completed by clicking the toggle-all checkbox.
initialize()
Weâve bound callbacks to several additional events:
Weâve bound a
filterOne()
callback on the Todos collection for achange:completed
event. This listens for changes to the completed flag for any model in the collection. The affected todo is passed to the callback, which triggers a customvisible
event on the model.Weâve bound a
filterAll()
callback for afilter
event, which works a little likeaddOne()
andaddAll()
. Its responsibility is to toggle which todo items are visible based on the filter currently selected in the UI (all, completed, or remaining) via calls tofilterOne()
.Weâve used the special
all
event to bind any event triggered on the Todos collection to the viewâs render method (discussed momentarily).
The initialize()
method completes
by fetching the previously saved todos from localStorage.
Several things are happening in our render()
method:
The
#main
and#footer
sections are displayed or hidden depending on whether there are any todos in the collection.The footer is populated with the HTML produced by instantiating the
statsTemplate
with the number of completed and remaining todo items.The HTML produced by the preceding step contains a list of filter links. The value of
app.TodoFilter
, which will be set by our router, is being used to apply the class selected to the link corresponding to the currently selected filter. This will result in conditional CSS styling being applied to that filter.The
allCheckbox
is updated based on whether there are remaining todos.
Now letâs look at the TodoView
view. This will be in charge of individual todo records, making sure the
view updates when the todo does. To enable this functionality, we will add
event listeners to the view that listen for events on an individual todoâs
HTML representation.
// js/views/todos.js
var
app
=
app
||
{};
// Todo Item View
// --------------
// The DOM element for a todo item...
app
.
TodoView
=
Backbone
.
View
.
extend
({
//... is a list tag.
tagName
:
'li'
,
// Cache the template function for a single item.
template
:
_
.
template
(
$
(
'#item-template'
).
html
()
),
// The DOM events specific to an item.
events
:
{
'dblclick label'
:
'edit'
,
'keypress .edit'
:
'updateOnEnter'
,
'blur .edit'
:
'close'
},
// The TodoView listens for changes to its model, rerendering. Since there's
// a one-to-one correspondence between a **Todo** and a **TodoView** in this
// app, we set a direct reference on the model for convenience.
initialize
:
function
()
{
this
.
listenTo
(
this
.
model
,
'change'
,
this
.
render
);
},
// Rerenders the titles of the todo item.
render
:
function
()
{
this
.
$el
.
html
(
this
.
template
(
this
.
model
.
toJSON
()
)
);
this
.
$input
=
this
.
$
(
'.edit'
);
return
this
;
},
// Switch this view into `"editing"` mode, displaying the input field.
edit
:
function
()
{
this
.
$el
.
addClass
(
'editing'
);
this
.
$input
.
focus
();
},
// Close the `"editing"` mode, saving changes to the todo.
close
:
function
()
{
var
value
=
this
.
$input
.
val
().
trim
();
if
(
value
)
{
this
.
model
.
save
({
title
:
value
});
}
this
.
$el
.
removeClass
(
'editing'
);
},
// If you hit `enter`, we're through editing the item.
updateOnEnter
:
function
(
e
)
{
if
(
e
.
which
===
ENTER_KEY
)
{
this
.
close
();
}
}
});
In the initialize()
constructor,
we set up a listener that monitors a Todo modelâs change
event. As a result, when the todo gets
updated, the application will rerender the view and visually reflect its
changes. Note that the model passed in the arguments hash by our
AppView
is automatically available to us as this.model
.
In the render()
method, we render
our Underscore.js #item-template
, which
was previously compiled into this.template
using
Underscoreâs _.template()
method. This
returns an HTML fragment that replaces the content of the viewâs element
(an li
element was implicitly created for us based on
the tagName
property). In other words,
the rendered template is now present under this.el
and can be appended to the todo list in
the user interface. render()
finishes
by caching the input element within the instantiated template into
this.input
.
Our events
hash includes three
callbacks:
edit()
Changes the current view into editing mode when a user double-clicks an existing item in the todo list. This allows the user to change the existing value of the itemâs
title
attribute.updateOnEnter()
Checks that the user has pressed the Return/Enter key and executes the
close()
function.close()
Trims the value of the current text in our
<input/>
field, ensuring that we donât process it further if it does not contain any text (for example, ââ). If a valid value has been provided, we save the changes to the current Todo model and close editing mode by removing the corresponding CSS class.
So now we have two views: AppView
and TodoView
. The former needs to be
instantiated on page load so its code gets executed. We can accomplish
this through jQueryâs ready()
utility,
which will execute a function when the DOM is loaded.
// js/app.js
var
app
=
app
||
{};
var
ENTER_KEY
=
13
;
$
(
function
()
{
// Kick things off by creating the **App**.
new
app
.
AppView
();
});
Letâs pause and ensure that the work weâve done so far functions as intended.
If you are following along, open file://*path*/index.html
in your
browser and monitor its console. If all is well, you shouldnât see any
JavaScript errors other than regarding the router.js
file that we havenât created yet. The todo list should be blank as we
havenât yet created any todos. Plus, there is some additional work weâll
need to do before the user interface fully functions.
However, a few things can be tested through the JavaScript console.
In the console, add a new todo item: window.app.Todos.create({ title: 'My first Todo
items'});
and press return (see Figure 4-2).
If all is functioning properly, this should log the new todo weâve just added to the Todos collection. The newly created todo is also saved to localStorage and will be available on page refresh.
window.app.Todos.create()
executes a collection method, Collection.create(attributes, [options])
, that
instantiates a new model item of the type passed into the collection
definitionâin our case, app.Todo
:
// from our js/collections/todos.js
var
TodoList
=
Backbone
.
Collection
.
extend
({
model
:
app
.
Todo
// the model type used by collection.create() to
// instantiate new model in the collection
...
)};
Run the following in the console to check it out:
var
secondTodo
=
window
.
app
.
Todos
.
create
({
title
:
'My second Todo item'
});
secondTodo
instanceof
app
.
Todo
// returns true
Now refresh the page; we should be able to see the fruits of our labor.
The todos added through the console should still appear in the list since they are populated from the localStorage. Also, we should be able to create a new todo by typing a title and pressing Enter (Figure 4-3).
Excellentâweâre making great progress, but what about completing and deleting todos?
The next part of our tutorial is going to cover completing and
deleting todos. These two actions are specific to each todo item, so we
need to add this functionality to the TodoView
view. We
will do so by adding togglecompleted()
and clear()
methods along with
corresponding entries in the events
hash.
// js/views/todos.js
var
app
=
app
||
{};
// Todo Item View
// --------------
// The DOM element for a todo item...
app
.
TodoView
=
Backbone
.
View
.
extend
({
//... is a list tag.
tagName
:
'li'
,
// Cache the template function for a single item.
template
:
_
.
template
(
$
(
'#item-template'
).
html
()
),
// The DOM events specific to an item.
events
:
{
'click .toggle'
:
'togglecompleted'
,
// NEW
'dblclick label'
:
'edit'
,
'click .destroy'
:
'clear'
,
// NEW
'keypress .edit'
:
'updateOnEnter'
,
'blur .edit'
:
'close'
},
// The TodoView listens for changes to its model, rerendering. Since there's
// a one-to-one correspondence between a **Todo** and a **TodoView** in this
// app, we set a direct reference on the model for convenience.
initialize
:
function
()
{
this
.
listenTo
(
this
.
model
,
'change'
,
this
.
render
);
this
.
listenTo
(
this
.
model
,
'destroy'
,
this
.
remove
);
// NEW
this
.
listenTo
(
this
.
model
,
'visible'
,
this
.
toggleVisible
);
// NEW
},
// Rerender the titles of the todo item.
render
:
function
()
{
this
.
$el
.
html
(
this
.
template
(
this
.
model
.
toJSON
()
)
);
this
.
$el
.
toggleClass
(
'completed'
,
this
.
model
.
get
(
'completed'
)
);
// NEW
this
.
toggleVisible
();
// NEW
this
.
$input
=
this
.
$
(
'.edit'
);
return
this
;
},
// NEW - Toggles visibility of item
toggleVisible
:
function
()
{
this
.
$el
.
toggleClass
(
'hidden'
,
this
.
isHidden
());
},
// NEW - Determines if item should be hidden
isHidden
:
function
()
{
var
isCompleted
=
this
.
model
.
get
(
'completed'
);
return
(
// hidden cases only
(
!
isCompleted
&&
app
.
TodoFilter
===
'completed'
)
||
(
isCompleted
&&
app
.
TodoFilter
===
'active'
)
);
},
// NEW - Toggle the `"completed"` state of the model.
togglecompleted
:
function
()
{
this
.
model
.
toggle
();
},
// Switch this view into `"editing"` mode, displaying the input field.
edit
:
function
()
{
this
.
$el
.
addClass
(
'editing'
);
this
.
$input
.
focus
();
},
// Close the `"editing"` mode, saving changes to the todo.
close
:
function
()
{
var
value
=
this
.
$input
.
val
().
trim
();
if
(
value
)
{
this
.
model
.
save
({
title
:
value
});
}
else
{
this
.
clear
();
// NEW
}
this
.
$el
.
removeClass
(
'editing'
);
},
// If you hit `enter`, we're through editing the item.
updateOnEnter
:
function
(
e
)
{
if
(
e
.
which
===
ENTER_KEY
)
{
this
.
close
();
}
},
// NEW - Remove the item, destroy the model from
// *localStorage* and delete its view.
clear
:
function
()
{
this
.
model
.
destroy
();
}
});
The key part of this is the two event handlers weâve added, a
togglecompleted
event on the todoâs checkbox, and a
click
event on the todoâs <button class="destroy" />
button.
Letâs look at the events that occur when we click the checkbox for a todo item:
The
togglecompleted()
function is invoked, which callstoggle()
on the Todo model.toggle()
toggles the completed status of the todo and callssave()
on the model.The save generates a
change
event on the model that is bound to ourTodoView
âsrender()
method. Weâve added a statement inrender()
that toggles the completed class on the element depending on the modelâs completed state. The associated CSS changes the color of the title text and strikes a line through it when the todo is completed.The save also results in a
change:completed
event on the model, which is handled by theAppView
âsfilterOne()
method. If we look back at theAppView
, we see thatfilterOne()
will trigger avisible
event on the model. This is used in conjunction with the filtering in our routes and collections so that we display an item only if its completed state falls in line with the current filter. In our update to theTodoView
, we bound the modelâs visible event to thetoggleVisible()
method. This method uses the newisHidden()
method to determine if the todo should be visible and updates it accordingly.
Now letâs look at what happens when we click on a todoâs destroy button:
The
clear()
method is invoked, which callsdestroy()
on the Todo model.The todo is deleted from localStorage and a
destroy
event is triggered.In our update to the
TodoView
, we bound the modelâsdestroy
event to the viewâs inheritedremove()
method. This method deletes the view and automatically removes the associated element from the DOM. Since we usedlistenTo()
to bind the viewâs listeners to its model,remove()
also unbinds the listening callbacks from the model, ensuring that a memory leak does not occur.destroy()
also removes the model from the Todos collection, which triggers aremove
event on the collection.Since the
AppView
has itsrender()
method bound toall
events on the Todos collection, that view is rendered and the stats in the footer are updated.
Thatâs all there is to it!
If you want to see an example of those, see the complete source.
Finally, we move on to routing, which will allow us to easily filter the list of items that are active as well as those that have been completed (shown in Figure 4-4). Weâll be supporting the following routes:
#/ (all - default) #/active #/completed
When the route changes, the todo list will be filtered on a model level and the selected class on the filter links in the footer will be toggled as just described. When an item is updated while a filter is active it will be updated accordingly (e.g., if the filter is active and the item is checked, it will be hidden). The active filter is persisted on reload.
// js/routers/router.js
// Todo Router
// ----------
var
Workspace
=
Backbone
.
Router
.
extend
({
routes
:
{
'*filter'
:
'setFilter'
},
setFilter
:
function
(
param
)
{
// Set the current filter to be used
// Trigger a collection filter event, causing hiding/unhiding
// of Todo view items
window
.
app
.
Todos
.
trigger
(
'filter'
);
}
});
app
.
TodoRouter
=
new
Workspace
();
Backbone
.
history
.
start
();
Our router uses a *splat
to set up a default
route that passes the string after #/
in the URL to
setFilter()
, which sets window.app.TodoFilter
to that string.
As we can see in the line window.app.Todos.trigger('filter')
, once the
filter has been set, we simply trigger filter on our Todos collection to
toggle which items are visible and which are hidden. Recall that our
AppView
âs filterAll()
method is bound to the collectionâs
filter event and that any event on the collection will cause the
AppView
to rerender.
Finally, we create an instance of our router and call Backbone.history.start()
to route the initial
URL during page load.
Weâve now built our first complete Backbone.js application. You can view the latest version of the full app online at any time, and the sources are readily available via TodoMVC.
In Chapter 8, weâll learn how to further modularize this application using RequireJS, swap out our persistence layer to a database backend, and finally unit-test the application with a few different testing frameworks.
Get Developing Backbone.js Applications now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.