TodoMVC Guide
This guide will walk you through building a slightly modified version of TodoMVC with CanJS’s Core libraries and can-fixture. It takes about 1 hour to complete.
Setup
The easiest way to get started is to clone the following JS Bin by clicking the JS Bin button on the top left:
The JS Bin starts with the static HTML and CSS a designer might turn over to a JS developer. We will be adding all the JavaScript functionality.
The JS Bin also loads can.all.js, which is a script that includes all of CanJS core, ecosystem, legacy and infrastructure libraries under a
single global can
namespace.
Generally speaking, you should not use the global can
script, but instead you
should import things directly with a module loader like StealJS,
WebPack or Browserify. In a real app, your code will look like:
var DefineMap = require("can-define/map/map");
var DefineList = require("can-define/list/list");
var Todo = DefineMap.extend({ ... });
Todo.List = DefineList.extend({ ... });
Not:
var Todo = can.DefineMap.extend({ ... });
Todo.List = can.DefineList.extend({ ... });
Read Setting Up CanJS for instructions on how to set up CanJS in a real app.
Create and render the template
In this section, we will render the markup in a can-stache live-bound template.
Update the HTML tab to have a <script>
tag around the html content.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="TodoMVC Guide 3.0 Start">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type="text/stache" id="todomvc-template">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?">
</header>
<section id="main" class="">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
<li class="todo">
<div class="view">
<input class="toggle" type="checkbox">
<label>Do the dishes</label>
<button class="destroy"></button>
</div>
<input class="edit" type="text" value="Do the dishes">
</li>
<li class="todo completed">
<div class="view">
<input class="toggle" type="checkbox">
<label>Mow the lawn</label>
<button class="destroy"></button>
</div>
<input class="edit" type="text" value="Mow the lawn">
</li>
<li class="todo editing">
<div class="view">
<input class="toggle" type="checkbox">
<label>Pick up dry cleaning</label>
<button class="destroy"></button>
</div>
<input class="edit" type="text" value="Pick up dry cleaning">
</li>
</ul>
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>2</strong> 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>
<button id="clear-completed">
Clear completed (1)
</button>
</footer>
</section>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>
</body>
</html>
Update the JavaScript
tab to:
- Use can-stache.from to load the contents of the
<script>
tag as a template renderer function. - Render the template with an empty object into a document fragment.
- Insert the fragment into the document’s
<body>
element.
To load, render, and add this template to the
body, add the following to the JavaScript
tab:
var template = can.stache.from("todomvc-template");
var frag = template({});
document.body.appendChild(frag);
When complete, you should see the same content as before. Only now, it’s rendered with a live-bound stache template. The live binding means that when the template’s data is changed, it will update automatically. We’ll see that in the next step.
Define the todos type and show the active and complete count
In this section, we will:
- Create a list of todos and show them.
- Show the number of active (
complete === true
) and complete todos. - Connect a todo’s
complete
property to a checkbox so that when we toggle the checkbox the number of active and complete todos changes.
Update the JavaScript
tab to:
- Define a
Todo
type with can-define/map/map. - Define a
Todo.List
type along with anactive
andcomplete
property with can-define/list/list. - Create a list of todos and pass those to the template.
var Todo = can.DefineMap.extend({
id: "number",
name: "string",
complete: {type: "boolean", value: false}
});
Todo.List = can.DefineList.extend({
"#": Todo,
get active(){
return this.filter({complete: false});
},
get complete(){
return this.filter({complete: true});
}
});
var todos = new Todo.List([
{ id: 5, name: "mow lawn", complete: false },
{ id: 6, name: "dishes", complete: true },
{ id: 7, name: "learn canjs", complete: false }
]);
var template = can.stache.from("todomvc-template");
var frag = template({todos: todos});
document.body.appendChild(frag);
Update the HTML tab to:
- Use
{{#each(todos)}}
to loop through every todo. - Add
completed
to the<li>
’sclassName
if the<li>
’s todo is complete. - Use
checked:bind
to two-way bind the checkbox’schecked
property to its todo’scomplete
property. - Use
{{name}}
to insert the value todo’sname
as the content of the<label>
andvalue
of the text<input/>
. - Insert the active and complete number of todos.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="TodoMVC Guide 3.0 - Create and render the template">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type="text/stache" id="todomvc-template">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?">
</header>
<section id="main" class="">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
{{#each(todos)}}
<li class="todo {{#if(complete)}}completed{{/if}}">
<div class="view">
<input class="toggle" type="checkbox" checked:bind="complete">
<label>{{name}}</label>
<button class="destroy"></button>
</div>
<input class="edit" type="text" value="{{name}}"/>
</li>
{{/each}}
</ul>
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{todos.active.length}}</strong> 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>
<button id="clear-completed">
Clear completed ({{todos.complete.length}})
</button>
</footer>
</section>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>
</body>
</html>
When complete, you should be able to toggle the checkboxes and see the number of items left and the completed count change automatically. This is because can-stache is able to listen for changes in observables like can-define/map/map, can-define/list/list and can-compute.
Get todos from the server
In this section, we will:
- Load todos from a RESTful service.
- Fake that RESTful service.
Update the JavaScript
tab to:
- Define what the RESTful service layer’s parameters are with can-set.Algebra.
- Create a fake data store that is initialized with data for 3 todos with store.
- Trap AJAX requests to
"/api/todos"
and provide responses with the data from the fake data store with can-fixture. - Connect the
Todo
andTodo.List
types to the RESTful"/api/todos"
endpoint using can-connect/can/super-map/super-map. This allows you to load, create, update, and destroy todos on the server. - Use getList to load a list of all todos on the server. The result
of
getList
is a Promise that resolves to aTodo.List
with the todos returned from the fake data store. ThatPromise
is passed to the template astodosPromise
.
var todoAlgebra = new can.set.Algebra(
can.set.props.boolean("complete"),
can.set.props.id("id"),
can.set.props.sort("sort")
);
var todoStore = can.fixture.store([
{ name: "mow lawn", complete: false, id: 5 },
{ name: "dishes", complete: true, id: 6 },
{ name: "learn canjs", complete: false, id: 7 }
], todoAlgebra);
can.fixture("/api/todos", todoStore);
can.fixture.delay = 1000;
var Todo = can.DefineMap.extend({
id: "number",
name: "string",
complete: {type: "boolean", value: false}
});
Todo.List = can.DefineList.extend({
"#": Todo,
get active(){
return this.filter({complete: false});
},
get complete(){
return this.filter({complete: true});
}
});
can.connect.superMap({
url: "/api/todos",
Map: Todo,
List: Todo.List,
name: "todo",
algebra: todoAlgebra
});
var template = can.stache.from("todomvc-template");
var frag = template({todosPromise: Todo.getList({})});
document.body.appendChild(frag);
Update the HTML tab to:
- Use
{{#each(todosPromise.value)}}
to loop through the promise’s resolved value, which is the list of todos returned by the server. - Read the active and completed number of todos from the promise’s resolved value.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="TodoMVC Guide 3.0 - Create the todos type and get items left working">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type="text/stache" id="todomvc-template">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?">
</header>
<section id="main" class="">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
{{#each(todosPromise.value)}}
<li class="todo {{#if(complete)}}completed{{/if}}">
<div class="view">
<input class="toggle" type="checkbox" checked:bind="complete">
<label>{{name}}</label>
<button class="destroy"></button>
</div>
<input class="edit" type="text" value="{{name}}"/>
</li>
{{/each}}
</ul>
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{todosPromise.value.active.length}}</strong> 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>
<button id="clear-completed">
Clear completed ({{todosPromise.value.complete.length}})
</button>
</footer>
</section>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>
</body>
</html>
When complete, you’ll notice a 1 second delay before seeing the list of todos as
they load for the first time from the fixtured data store. On future page reloads, the
list of todos will load immediately. This is because can-connect/can/super-map/super-map adds the fall-through-cache behavior. The
fall-through-cache behavior stores loaded data in
localStorage
. Future requests will hit localStorage
for data first and present that data
to the user before making a request to the server and updating the original data with
any changes. Use localStorage.clear()
to see the difference.
Destroy todos
In this section, we will:
- Delete a todo on the server when its destroy button is clicked.
- Remove the todo from the page after it’s deleted.
Update the HTML tab to:
- Add
destroying
to the<li>
’sclassName
if the<li>
’s todo is being destroyed using isDestroying. - Call the
todo
’s destroy method when the<button>
is clicked usingon:click
.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="TodoMVC Guide 3.0 - Destroy todos">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type="text/stache" id="todomvc-template">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?">
</header>
<section id="main" class="">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
{{#each(todosPromise.value)}}
<li class="todo {{#if(complete)}}completed{{/if}}
{{#if(isDestroying)}}destroying{{/if}}">
<div class="view">
<input class="toggle" type="checkbox" checked:bind="complete">
<label>{{name}}</label>
<button class="destroy" on:click="destroy()"></button>
</div>
<input class="edit" type="text" value="{{name}}"/>
</li>
{{/each}}
</ul>
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{todosPromise.value.active.length}}</strong> 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>
<button id="clear-completed">
Clear completed ({{todosPromise.value.complete.length}})
</button>
</footer>
</section>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>
</body>
</html>
When complete, you should be able to delete a todo by clicking its delete button. After clicking the todo, its name will turn red and italic. Once deleted, the todo will be automatically removed from the page.
The deleted todo is automatically removed from the page because can-connect/can/super-map/super-map adds the real-time behavior. The
real-time behavior automatically updates lists (like Todo.List
) when instances
are created, updated or destroyed. If you’ve created the right Algebra, you
shouldn’t have to manage lists yourself.
Finally, if you click “Run with JS” after deleting a todo, you’ll notice the page temporarily shows fewer items. This is because the fall-through cache’s data is shown before the response from fixtured data store is used.
Create todos
In this section, we will:
- Create a custom element that can create todos on the server.
- Use that custom element.
Update the JavaScript
tab to:
- Use can-define/map/map to create a
TodoCreateVM
view model with:- A
todo
property that holds a newTodo
instance. - A
createTodo
method that saves theTodo
instance and replaces it with a new one once saved.
- A
- Use can-component to create a custom
<todo-create>
component that renders thetodo-create-template
template with an instance of theTodoCreateVM
.
var todoAlgebra = new can.set.Algebra(
can.set.props.boolean("complete"),
can.set.props.id("id"),
can.set.props.sort("sort")
);
var todoStore = can.fixture.store([
{ name: "mow lawn", complete: false, id: 5 },
{ name: "dishes", complete: true, id: 6 },
{ name: "learn canjs", complete: false, id: 7 }
], todoAlgebra);
can.fixture("/api/todos", todoStore);
can.fixture.delay = 1000;
var Todo = can.DefineMap.extend({
id: "number",
name: "string",
complete: {type: "boolean", value: false}
});
Todo.List = can.DefineList.extend({
"#": Todo,
get active(){
return this.filter({complete: false});
},
get complete(){
return this.filter({complete: true});
}
});
can.connect.superMap({
url: "/api/todos",
Map: Todo,
List: Todo.List,
name: "todo",
algebra: todoAlgebra
});
var TodoCreateVM = can.DefineMap.extend({
todo: {Value: Todo},
createTodo: function(){
this.todo.save().then(function(){
this.todo = new Todo();
}.bind(this));
}
});
can.Component.extend({
tag: "todo-create",
view: can.stache.from("todo-create-template"),
ViewModel: TodoCreateVM
});
var template = can.stache.from("todomvc-template");
var frag = template({todosPromise: Todo.getList({})});
document.body.appendChild(frag);
Update the HTML tab to:
- Create the
todo-create-template
that:- Updates the
todo
’sname
with the<input>
’svalue
usingvalue:bind
. - Calls
createTodo
when theenter
key is pressed usingon:enter
.
- Updates the
- Use
<todo-create/>
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="TodoMVC Guide 3.0 - Create todos">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type='text/stache' id='todo-create-template'>
<input id="new-todo"
placeholder="What needs to be done?"
value:bind="todo.name"
on:enter="createTodo()"/>
</script>
<script type="text/stache" id="todomvc-template">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<todo-create/>
</header>
<section id="main" class="">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
{{#each(todosPromise.value)}}
<li class="todo {{#if(complete)}}completed{{/if}}
{{#if(isDestroying)}}destroying{{/if}}">
<div class="view">
<input class="toggle" type="checkbox" checked:bind="complete">
<label>{{name}}</label>
<button class="destroy" on:click="destroy()"></button>
</div>
<input class="edit" type="text" value="{{name}}"/>
</li>
{{/each}}
</ul>
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{todosPromise.value.active.length}}</strong> 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>
<button id="clear-completed">
Clear completed ({{todosPromise.value.complete.length}})
</button>
</footer>
</section>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>
</body>
</html>
When complete, you will be able to create a todo by typing the name of the todo and pressing
enter
. Notice that the new todo automatically appears in the list of todos. This
is also because can-connect/can/super-map/super-map adds the real-time behavior. The
real-time behavior automatically inserts newly created items into
lists that they belong within.
List todos
In this section, we will:
- Define a custom element for showing a list of todos.
- Use that custom element by passing it the list of fetched todos.
Update the JavaScript
tab to:
- Create a
TodoListVM
view model type which has atodos
property of typeTodo.List
. - Use can-component to define a
<todo-list>
element.
var todoAlgebra = new can.set.Algebra(
can.set.props.boolean("complete"),
can.set.props.id("id"),
can.set.props.sort("sort")
);
var todoStore = can.fixture.store([
{ name: "mow lawn", complete: false, id: 5 },
{ name: "dishes", complete: true, id: 6 },
{ name: "learn canjs", complete: false, id: 7 }
], todoAlgebra);
can.fixture("/api/todos", todoStore);
can.fixture.delay = 1000;
var Todo = can.DefineMap.extend({
id: "number",
name: "string",
complete: {type: "boolean", value: false}
});
Todo.List = can.DefineList.extend({
"#": Todo,
get active(){
return this.filter({complete: false});
},
get complete(){
return this.filter({complete: true});
}
});
can.connect.superMap({
url: "/api/todos",
Map: Todo,
List: Todo.List,
name: "todo",
algebra: todoAlgebra
});
var TodoCreateVM = can.DefineMap.extend({
todo: {Value: Todo},
createTodo: function(){
this.todo.save().then(function(){
this.todo = new Todo();
}.bind(this));
}
});
can.Component.extend({
tag: "todo-create",
view: can.stache.from("todo-create-template"),
ViewModel: TodoCreateVM
});
var TodoListVM = can.DefineMap.extend({
todos: Todo.List
});
can.Component.extend({
tag: "todo-list",
view: can.stache.from("todo-list-template"),
ViewModel: TodoListVM
});
var template = can.stache.from("todomvc-template");
var frag = template({todosPromise: Todo.getList({})});
document.body.appendChild(frag);
Update the HTML tab to:
- Create the
todo-list-template
that loops through a list oftodos
(instead oftodosPromise.value
). - Create a
<todo-list>
element and set itstodos
property to the resolved value oftodosPromise
using{todos}
.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="TodoMVC Guide 3.0 - List todos">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type='text/stache' id='todo-create-template'>
<input id="new-todo"
placeholder="What needs to be done?">
value:bind="todo.name"
on:enter="createTodo()"/>
</script>
<script type='text/stache' id='todo-list-template'>
<ul id="todo-list">
{{#each(todos)}}
<li class="todo {{#if(complete)}}completed{{/if}}
{{#if(isDestroying)}}destroying{{/if}}">
<div class="view">
<input class="toggle" type="checkbox" checked:bind="complete">
<label>{{name}}</label>
<button class="destroy" on:click="destroy()"></button>
</div>
<input class="edit" type="text" value="{{name}}"/>
</li>
{{/each}}
</ul>
</script>
<script type="text/stache" id="todomvc-template">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<todo-create/>
</header>
<section id="main" class="">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<todo-list todos:from="todosPromise.value"/>
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{todosPromise.value.active.length}}</strong> 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>
<button id="clear-completed">
Clear completed ({{todosPromise.value.complete.length}})
</button>
</footer>
</section>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>
</body>
</html>
When complete, everything should work the same. We didn’t add any new functionality, we just moved code around to make it more isolated, potentially reusable, and more maintainable.
Edit todos
In this section, we will:
- Make it possible to edit a todo’s
name
and save that change to the server.
Update the JavaScript
tab to:
- Update the
TodoListVM
to include the methods and properties needed to edit a todo’s name, including:- An
editing
property of typeTodo
that stores which todo is being edited. - A
backupName
property that stores the todo’s name before being edited. - An
edit
method that sets up the editing state. - A
cancelEdit
method that undos the editing state if in the editing state. - An
updateName
method that updates the editing todo and saves it to the server.
- An
var todoAlgebra = new can.set.Algebra(
can.set.props.boolean("complete"),
can.set.props.id("id"),
can.set.props.sort("sort")
);
var todoStore = can.fixture.store([
{ name: "mow lawn", complete: false, id: 5 },
{ name: "dishes", complete: true, id: 6 },
{ name: "learn canjs", complete: false, id: 7 }
], todoAlgebra);
can.fixture("/api/todos", todoStore);
can.fixture.delay = 1000;
var Todo = can.DefineMap.extend({
id: "number",
name: "string",
complete: {type: "boolean", value: false}
});
Todo.List = can.DefineList.extend({
"#": Todo,
get active(){
return this.filter({complete: false});
},
get complete(){
return this.filter({complete: true});
}
});
can.connect.superMap({
url: "/api/todos",
Map: Todo,
List: Todo.List,
name: "todo",
algebra: todoAlgebra
});
var TodoCreateVM = can.DefineMap.extend({
todo: {Value: Todo},
createTodo: function(){
this.todo.save().then(function(){
this.todo = new Todo();
}.bind(this));
}
});
can.Component.extend({
tag: "todo-create",
view: can.stache.from("todo-create-template"),
ViewModel: TodoCreateVM
});
var TodoListVM = can.DefineMap.extend({
todos: Todo.List,
editing: Todo,
backupName: "string",
isEditing: function(todo){
return todo === this.editing;
},
edit: function(todo){
this.backupName = todo.name;
this.editing = todo;
},
cancelEdit: function(){
if(this.editing) {
this.editing.name = this.backupName;
}
this.editing = null;
},
updateName: function() {
this.editing.save();
this.editing = null;
}
});
can.Component.extend({
tag: "todo-list",
view: can.stache.from("todo-list-template"),
ViewModel: TodoListVM
});
var template = can.stache.from("todomvc-template");
var frag = template({todosPromise: Todo.getList({})});
document.body.appendChild(frag);
Update the HTML tab to:
- Use the
isEditing
method to addediting
to theclassName
of the<li>
being edited. - When the checkbox changes, update the todo on the server with save,
- Call
edit
with the current context using this. - Set up the edit input to:
- Two-way bind its value to the current todo’s
name
usingvalue:bind
. - Call
updateName
when the enter key is pressed usingon:enter
. - Focus the input when
isEditing
is true using the special focused attribute. - Call
cancelEdit
if the input element loses focus.
- Two-way bind its value to the current todo’s
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="TodoMVC Guide 3.0 - List todos">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type='text/stache' id='todo-create-template'>
<input id="new-todo"
placeholder="What needs to be done?"
value:bind="todo.name"
on:enter="createTodo()"/>
</script>
<script type='text/stache' id='todo-list-template'>
<ul id="todo-list">
{{#each(todos)}}
<li class="todo {{#if(complete)}}completed{{/if}}
{{#if(isDestroying)}}destroying{{/if}}
{{#if(isEditing(this))}}editing{{/if}}">
<div class="view">
<input class="toggle" type="checkbox"
checked:bind="complete" on:change="save()">
<label on:dblclick="edit(this)">{{name}}</label>
<button class="destroy" on:click="destroy()"></button>
</div>
<input class="edit" type="text"
value:bind="name"
on:enter="updateName()"
focused:from="isEditing(this)"
on:blur="cancelEdit()"/>
</li>
{{/each}}
</ul>
</script>
<script type="text/stache" id="todomvc-template">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<todo-create/>
</header>
<section id="main" class="">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<todo-list todos:from="todosPromise.value"/>
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{todosPromise.value.active.length}}</strong> 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>
<button id="clear-completed">
Clear completed ({{todosPromise.value.complete.length}})
</button>
</footer>
</section>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>
</body>
</html>
When complete, you should be able to edit a todo’s name.
Routing
In this section, we will:
- Make it possible to use the browser’s forwards and backwards buttons to change between showing all todos, only active todos, or only completed todos.
- Add links to change between showing all todos, only active todos, or only completed todos.
- Make those links bold when the site is currently showing that link.
Update the JavaScript
tab to:
- Create a
AppVM
view model type that will manage the behavior of thetodomvc-template
and will update when the url changes.- Define a
filter
property that will be updated when the route changes. - Define a
route
property that will be updated when the route changes. - Define a
todosPromise
property that usesfilter
to determine what data should be loaded from the server.- If
filter
is falsey, all data will be loaded. - If
filter
is"complete"
, only complete todos will be loaded. - If
filter
is any other value, the active todos will be loaded.
- If
- Define a
- Create an instance of the application view model (
appVM
). - Connect changes in the url to changes in the
appVM
with data. - Create a pretty routing rule so if the url looks like
"#!active"
, thefilter
property ofappVM
will be set tofilter
with can-route. - Initialize the url’s values on
appVM
and set up the two-way connection with ready. - Render the
todomvc-template
with theappVM
.
var todoAlgebra = new can.set.Algebra(
can.set.props.boolean("complete"),
can.set.props.id("id"),
can.set.props.sort("sort")
);
var todoStore = can.fixture.store([
{ name: "mow lawn", complete: false, id: 5 },
{ name: "dishes", complete: true, id: 6 },
{ name: "learn canjs", complete: false, id: 7 }
], todoAlgebra);
can.fixture("/api/todos", todoStore);
can.fixture.delay = 1000;
var Todo = can.DefineMap.extend({
id: "number",
name: "string",
complete: {type: "boolean", value: false}
});
Todo.List = can.DefineList.extend({
"#": Todo,
get active(){
return this.filter({complete: false})
},
get complete(){
return this.filter({complete: true});
}
});
can.connect.superMap({
url: "/api/todos",
Map: Todo,
List: Todo.List,
name: "todo",
algebra: todoAlgebra
});
var TodoCreateVM = can.DefineMap.extend({
todo: {Value: Todo},
createTodo: function(){
this.todo.save().then(function(){
this.todo = new Todo();
}.bind(this));
}
});
can.Component.extend({
tag: "todo-create",
view: can.stache.from("todo-create-template"),
ViewModel: TodoCreateVM
});
var TodoListVM = can.DefineMap.extend({
todos: Todo.List,
editing: Todo,
backupName: "string",
isEditing: function(todo){
return todo === this.editing;
},
edit: function(todo){
this.backupName = todo.name;
this.editing = todo;
},
cancelEdit: function(){
if(this.editing) {
this.editing.name = this.backupName;
}
this.editing = null;
},
updateName: function() {
this.editing.save();
this.editing = null;
}
});
can.Component.extend({
tag: "todo-list",
view: can.stache.from("todo-list-template"),
ViewModel: TodoListVM
});
var AppVM = can.DefineMap.extend({
filter: "string",
get todosPromise(){
if(!this.filter) {
return Todo.getList({});
} else {
return Todo.getList({complete: this.filter === "complete"});
}
}
});
var appVM = new AppVM();
can.route.data = appVM;
can.route("{filter}");
can.route.ready();
var template = can.stache.from("todomvc-template");
var frag = template(appVM);
document.body.appendChild(frag);
Update the HTML tab to:
- Set
href
to a url that will set the desired properties onappVM
when clicked. - Add
class='selected'
to the link if the current route matches the current properties of theappVM
using {{#routeCurrent(hash)}}.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="TodoMVC Guide 3.0 - Routing">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type='text/stache' id='todo-create-template'>
<input id="new-todo"
placeholder="What needs to be done?"
value:bind="todo.name"
on:enter="createTodo()"/>
</script>
<script type='text/stache' id='todo-list-template'>
<ul id="todo-list">
{{#each(todos)}}
<li class="todo {{#if(complete)}}completed{{/if}}
{{#if(isDestroying)}}destroying{{/if}}
{{#if(isEditing(this))}}editing{{/if}}">
<div class="view">
<input class="toggle" type="checkbox"
checked:bind="complete" on:change="save()">
<label on:dblclick="edit(this)">{{name}}</label>
<button class="destroy" on:click="destroy()"></button>
</div>
<input class="edit" type="text"
value:bind="name"
on:enter="updateName()"
focused:from="isEditing(this)"
on:blur="cancelEdit()"/>
</li>
{{/each}}
</ul>
</script>
<script type="text/stache" id="todomvc-template">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<todo-create/>
</header>
<section id="main" class="">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<todo-list todos:from="todosPromise.value"/>
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{todosPromise.value.active.length}}</strong> items left
</span>
<ul id="filters">
<li>
<a href="{{routeUrl(filter=undefined)}}"
{{#routeCurrent(filter=undefined)}}class='selected'{{/routeCurrent}}>All</a>
</li>
<li>
<a href="{{routeUrl(filter='active')}}"
{{#routeCurrent(filter='active')}}class='selected'{{/routeCurrent}}>Active</a>
</li>
<li>
<a href="{{routeUrl(filter='complete')}}"
{{#routeCurrent(filter='complete')}}class='selected'{{/routeCurrent}}>Completed</a>
</li>
</ul>
<button id="clear-completed">
Clear completed ({{todosPromise.value.complete.length}})
</button>
</footer>
</section>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>
</body>
</html>
When complete, you should be able to click the All
, Active
, and Completed
links and
see the right data. When you click from All
to Active
or from All
to Completed
,
you’ll notice that the list of todos is updated immediately, despite a request being made.
This is because the fall-through-cache is able to make use
of the data loaded for the All
todos page. It’s able to filter out the Active
and
Completed
data.
Toggle all and clear completed
In this section, we will:
- Make the
toggle-all
button change all todos to complete or incomplete. - Make the
clear-completed
button delete all complete todos.
Update the JavaScript
tab to:
- Add the following properties and methods to
Todo.List
:- An
allComplete
property that returnstrue
if every todo is complete. - A
saving
property that returns todos that are being saved using isSaving. - An
updateCompleteTo
method that updates every todo’scomplete
property to the specified value and updates the compute on the server with save. - A
destroyComplete
method that deletes every complete todo with destroy.
- An
- Adds the following properties to
AppVM
:- A
todosList
property that gets its value from thetodosPromise
using an asynchronous getter. - An
allChecked
property that returnstrue
if every todo is complete. The property can also be set totrue
orfalse
and it will set every todo to that value.
- A
var todoAlgebra = new can.set.Algebra(
can.set.props.boolean("complete"),
can.set.props.id("id"),
can.set.props.sort("sort")
);
var todoStore = can.fixture.store([
{ name: "mow lawn", complete: false, id: 5 },
{ name: "dishes", complete: true, id: 6 },
{ name: "learn canjs", complete: false, id: 7 }
], todoAlgebra);
can.fixture("/api/todos", todoStore);
can.fixture.delay = 1000;
var Todo = can.DefineMap.extend({
id: "number",
name: "string",
complete: {type: "boolean", value: false}
});
Todo.List = can.DefineList.extend({
"#": Todo,
get active(){
return this.filter({complete: false});
},
get complete(){
return this.filter({complete: true});
},
get allComplete(){
return this.length === this.complete.length;
},
get saving(){
return this.filter(function(todo){
return todo.isSaving();
});
},
updateCompleteTo: function(value){
this.forEach(function(todo){
todo.complete = value;
todo.save();
});
},
destroyComplete: function(){
this.complete.forEach(function(todo){
todo.destroy();
});
}
});
can.connect.superMap({
url: "/api/todos",
Map: Todo,
List: Todo.List,
name: "todo",
algebra: todoAlgebra
});
var TodoCreateVM = can.DefineMap.extend({
todo: {Value: Todo},
createTodo: function(){
this.todo.save().then(function(){
this.todo = new Todo();
}.bind(this));
}
});
can.Component.extend({
tag: "todo-create",
view: can.stache.from("todo-create-template"),
ViewModel: TodoCreateVM
});
var TodoListVM = can.DefineMap.extend({
todos: Todo.List,
editing: Todo,
backupName: "string",
isEditing: function(todo){
return todo === this.editing;
},
edit: function(todo){
this.backupName = todo.name;
this.editing = todo;
},
cancelEdit: function(){
if(this.editing) {
this.editing.name = this.backupName;
}
this.editing = null;
},
updateName: function() {
this.editing.save();
this.editing = null;
}
});
can.Component.extend({
tag: "todo-list",
view: can.stache.from("todo-list-template"),
ViewModel: TodoListVM
});
var AppVM = can.DefineMap.extend({
filter: "string",
get todosPromise(){
if(!this.filter) {
return Todo.getList({});
} else {
return Todo.getList({complete: this.filter === "complete"});
}
},
todosList: {
get: function(lastSetValue, resolve){
this.todosPromise.then(resolve);
}
},
get allChecked(){
return this.todosList && this.todosList.allComplete;
},
set allChecked(newVal){
this.todosList && this.todosList.updateCompleteTo(newVal);
}
});
var appVM = new AppVM();
can.route.data = appVM;
can.route("{filter}");
can.route.ready();
var template = can.stache.from("todomvc-template");
var frag = template(appVM);
document.body.appendChild(frag);
Update the HTML tab to:
- Cross bind the
toggle-all
’schecked
property to theappVM
’sallChecked
property. - Disable the
toggle-all
button while any todo is saving. - Call the
Todo.List
’sdestroyComplete
method when theclear-completed
button is clicked on.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="TodoMVC Guide 3.0 - Routing">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<script type='text/stache' id='todo-create-template'>
<input id="new-todo"
placeholder="What needs to be done?"
value:bind="todo.name"
on:enter="createTodo()"/>
</script>
<script type='text/stache' id='todo-list-template'>
<ul id="todo-list">
{{#each(todos)}}
<li class="todo {{#if(./complete)}}completed{{/if}}
{{#if(isDestroying)}}destroying{{/if}}
{{#if(isEditing(this))}}editing{{/if}}">
<div class="view">
<input class="toggle" type="checkbox"
checked:bind="complete" on:change="save()">
<label on:dblclick="edit(this)">{{name}}</label>
<button class="destroy" on:click="destroy()"></button>
</div>
<input class="edit" type="text"
value:bind="name"
on:enter="updateName()"
focused:from="isEditing(this)"
on:blur="cancelEdit()"/>
</li>
{{/each}}
</ul>
</script>
<script type="text/stache" id="todomvc-template">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<todo-create/>
</header>
<section id="main" class="">
<input id="toggle-all" type="checkbox"
checked:bind="allChecked"
disabled:from="todosList.saving.length"/>
<label for="toggle-all">Mark all as complete</label>
<todo-list todos:from="todosPromise.value"/>
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{todosPromise.value.active.length}}</strong> items left
</span>
<ul id="filters">
<li>
<a href="{{routeUrl(filter=undefined)}}"
{{#routeCurrent(filter=undefined)}}class='selected'{{/routeCurrent}}>All</a>
</li>
<li>
<a href="{{routeUrl(filter='active')}}"
{{#routeCurrent(filter='active')}}class='selected'{{/routeCurrent}}>Active</a>
</li>
<li>
<a href="{{routeUrl(filter='complete')}}"
{{#routeCurrent(filter='complete')}}class='selected'{{/routeCurrent}}>Completed</a>
</li>
</ul>
<button id="clear-completed"
on:click="todosList.destroyComplete()">
Clear completed ({{todosPromise.value.complete.length}})
</button>
</footer>
</section>
</script>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>
</body>
</html>
When complete, you should be able to toggle all todos complete
state and
delete the completed todos. You should also have a really good idea how CanJS works!
Result
When finished, you should see something like the following JS Bin: