TodoMVC with StealJS
This tutorial walks through building TodoMVC with StealJS. It includes KeyNote presentations covering CanJS core libraries.
Setup (Framework Overview)
The problem
- Setup steal to load a basic CanJS application. A basic CanJS application has:
- A custom element defined by StacheElement and
an instance of that custom element in the page's HTML. That component includes:
- A set of ObservableObject like properties.
- A can-stache view that is rendered with the element properties.
- A custom element defined by StacheElement and
an instance of that custom element in the page's HTML. That component includes:
- In addition, this application should load the can-todomvc-test module version 6.0 and
pass it the custom element’s
props
. You will need to declare the version explicitly as different versions of this guide depend on different versions of this package.
What you need to know
To create a new project with StealJS, run:
npm init -y npm install steal@2 steal-tools@2 steal-css@1 --save-dev
To host static files, install
http-server
and run it like:npm install http-server -g http-server -c-1
If you load StealJS plugins, add them to your package.json configuration like:
"steal": { "plugins": [ "steal-css" ] }
Define a custom element with StacheElement:
import { StacheElement } from "can"; class TodoMVC extends StacheElement { static view = "..."; static props = {}; } customElements.define("todo-mvc", TodoMVC);
Load a view with the steal-stache plugin like:
import view from "./path/to/template.stache";
Note that steal-stache is a StealJS plugin and needs to be configured as such.
Add the custom element to your HTML page to see it in action:
<todo-mvc></todo-mvc>
Use the following HTML that a designer might have provided:
<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>
Use can-todomvc-test to load the application’s styles and run its tests:
import test from "can-todomvc-test"; test(appVM);
The solution
Create a folder:
mkdir todomvc
cd todomvc
Host it:
npm install http-server -g
http-server -c-1
Create a new project:
npm init -y
Install steal
, steal-tools
, and CanJS’s core modules:
npm install steal@2 steal-tools@2 steal-css@1 --save-dev
npm install can@6 steal-stache@5 --save-dev
Add steal.plugins to package.json:
{
"name": "todomvc",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"steal": "^2.0.2",
"steal-css": "^1.3.2",
"steal-tools": "^2.0.2"
},
"steal": {
"plugins": [
"steal-css",
"can"
]
},
"dependencies": {
"can": "^6.0.0"
}
}
Create the starting HTML page:
<!-- index.html -->
<todo-mvc></todo-mvc>
<script src="./node_modules/steal/steal.js" main></script>
Create the application template:
<!-- index.stache -->
<section id="todoapp">
<header id="header">
<h1>{{ this.appName }}</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>
Install the test harness:
npm install can-todomvc-test@6 --save-dev
Create the main app
// index.js
import { StacheElement } from "can";
import view from "./index.stache";
import test from "can-todomvc-test";
class TodoMVC extends StacheElement {
static view = view;
static props = {
appName: { default: "TodoMVC" }
};
}
customElements.define("todo-mvc", TodoMVC);
test(document.querySelector("todo-mvc"));
Define Todo type (ObservableObject basics)
The problem
- Define a
Todo
type as the export of models/todo.js, where:- It is an ObservableObject type.
- The id or name property values are coerced into a string.
- Its
complete
property is aBoolean
that defaults tofalse
. - It has a
toggleComplete
method that flipscomplete
to the opposite value.
Example test code:
const todo = new Todo({ id: 1, name: 2 });
QUnit.equal(todo.id, "1", "id is a string");
QUnit.equal(todo.name, "2", "name is a string");
QUnit.equal(todo.complete, false, "complete defaults to false");
todo.toggleComplete();
QUnit.equal(todo.complete, true, "toggleComplete works");
What you need to know
Extending ObservableObject defines a new
Type
.The DefinitionObject behavior defines a property’s type like:
class CustomType extends ObservableObject { static props = { propertyName: { type: Number } }; }
The default behavior defines a property’s initial value like:
class CustomType extends ObservableObject { static props = { propertyName: { default: 3 } }; }
Methods can be defined directly on the prototype like:
class CustomType extends ObservableObject { methodName() {} }
The solution
Create models/todo.js as follows:
// models/todo.js
import { ObservableObject, type } from "can";
export default class Todo extends ObservableObject {
static props = {
id: { type: type.convert(String) },
name: { type: type.convert(String) },
complete: {
type: type.convert(Boolean),
default: false
}
};
toggleComplete() {
this.complete = !this.complete;
}
}
Define TodoList type (ObservableArray basics)
The problem
- Define a
TodoList
on models/todo.js, where:- It is an ObservableArray type.
- The enumerable indexes are coerced into
Todo
types. - Its
.active
property returns a filteredTodoList
of the todos that are not complete. - Its
.complete
property returns a filteredTodoList
of the todos that are complete. - Its
.allComplete
property true if all the todos are complete.
Example test code:
QUnit.ok(TodoList, "Defined a List");
const todos = new TodoList([
{complete: true},
{},
{complete: true}
]);
QUnit.ok(todos[0] instanceof Todo, "each item in a TodoList is a Todo");
QUnit.equal(todos.active.length, 1);
QUnit.equal(todos.complete.length, 2);
QUnit.equal(todos.allComplete, false, "not allComplete");
todos[1].complete = true;
QUnit.equal(todos.allComplete, true, "allComplete");
What you need to know
Extending ObservableArray defines a new
ListType
.The items property defines the behavior of items in a list like:
class List extends ObservableArray { static items = ItemType; }
The get behavior defines observable computed properties like:
class CustomType extends ObservableObject { static props = { get propertyName() { return this.otherProperty; } }; }
filter can be used to filter a list into a new list:
list = new ListType([ // ... ]); list.filter(function(item) { return test(item); })
The solution
Update models/todo.js to the following:
// models/todo.js
import { ObservableArray, ObservableObject, type } from "can";
export default class Todo extends ObservableObject {
static props = {
id: { type: type.convert(String) },
name: { type: type.convert(String) },
complete: {
type: type.maybeConvert(Boolean),
default: false
}
};
toggleComplete() {
this.complete = !this.complete;
}
}
export class TodoList extends ObservableArray {
static items = type.convert(Todo);
static props = {
get active() {
return this.filter({
complete: false
});
},
get complete() {
return this.filter({
complete: true
});
},
get allComplete() {
return this.length === this.complete.length;
}
};
}
Render a list of todos (can-stache)
The problem
Add a
todosList
property whose default value will be aTodoList
with the following data:[ { name: "mow lawn", complete: false, id: 5 }, { name: "dishes", complete: true, id: 6 }, { name: "learn canjs", complete: false, id: 7 } ]
Write out an
<li>
for each todo intodosList
, including:- write the todo’s name in the
<label>
- add
completed
in the<li>
’sclass
if the todo iscomplete
. - check the todo’s checkbox if the todo is
complete
.
- write the todo’s name in the
Write out the number of items left and completed count in the “Clear completed” button.
What you need to know
CanJS uses can-stache to render data in a template and keep it live. Templates can be loaded with steal-stache.
A can-stache template uses {{key}} magic tags to insert data into the HTML output like:
{{something.name}}
Use {{# if(value) }} to do
if/else
branching in can-stache.Use {{# for(of) }} to do looping in can-stache.
The solution
Update index.js to the following:
// index.js
import { StacheElement } from "can";
import view from "./index.stache";
import { TodoList } from "~/models/todo";
import test from "can-todomvc-test";
class TodoMVC extends StacheElement {
static view = view;
static props = {
appName: { default: "TodoMVC" },
todosList: {
get default() {
return new TodoList([
{ name: "mow lawn", complete: false, id: 5 },
{ name: "dishes", complete: true, id: 6 },
{ name: "learn canjs", complete: false, id: 7 }
]);
}
}
};
}
customElements.define("todo-mvc", TodoMVC);
test(document.querySelector("todo-mvc"));
Update index.stache to the following:
<!-- index.stache -->
<section id="todoapp">
<header id="header">
<h1>{{ this.appName }}</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">
{{# for(todo of this.todosList) }}
<li class="todo {{# if(todo.complete) }}completed{{/ if }}">
<div class="view">
<input class="toggle" type="checkbox"
{{# if(todo.complete) }}checked{{/ if }}>
<label>{{ todo.name }}</label>
<button class="destroy"></button>
</div>
<input class="edit" type="text" value="{{ todo.name }}">
</li>
{{/ for }}
</ul>
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{ this.todosList.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 ({{ this.todosList.complete.length }})
</button>
</footer>
</section>
Toggle a todo’s completed state (event bindings)
The problem
- Call
toggleComplete
when a todo’s checkbox is clicked upon.
What you need to know
The can-stache-bindings Presentation’s DOM Event Bindings
Use on:EVENT to listen to an event on an element and call a method in can-stache. For example, the following calls
doSomething()
when the<div>
is clicked.<div on:click="doSomething()"> ... </div>
The solution
Update index.stache to the following:
<!-- index.stache -->
<section id="todoapp">
<header id="header">
<h1>{{ this.appName }}</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">
{{# for(todo of this.todosList) }}
<li class="todo {{# if(todo.complete) }}completed{{/ if }}">
<div class="view">
<input
class="toggle"
type="checkbox"
{{# if(todo.complete) }}checked{{/ if }}
on:click="todo.toggleComplete()"
>
<label>{{ todo.name }}</label>
<button class="destroy"></button>
</div>
<input class="edit" type="text" value="{{ todo.name }}">
</li>
{{/ for }}
</ul>
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{ this.todosList.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 ({{ this.todosList.complete.length }})
</button>
</footer>
</section>
Toggle a todo’s completed state (data bindings)
The problem
- Update a todo’s
complete
property when the checkbox’schecked
property changes with two-way bindings.
What you need to know
The can-stache-bindings Presentation’s DOM Data Bindings
Use value:bind to setup a two-way binding in can-stache. For example, the following keeps
todo.name
and the input’svalue
in sync:<input value:bind="todo.name">
The solution
Update index.stache to the following:
<!-- index.stache -->
<section id="todoapp">
<header id="header">
<h1>{{ this.appName }}</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">
{{# for(todo of this.todosList) }}
<li class="todo {{# if(todo.complete) }}completed{{/ if }}">
<div class="view">
<input class="toggle" type="checkbox" checked:bind="todo.complete">
<label>{{ todo.name }}</label>
<button class="destroy"></button>
</div>
<input class="edit" type="text" value="{{ todo.name }}">
</li>
{{/ for }}
</ul>
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{ this.todosList.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 ({{ this.todosList.complete.length }})
</button>
</footer>
</section>
Define Todo's identity
The problem
- CanJS’s model needs to know what is the unique identifier of a type.
The solution
Update models/todo.js to the following:
// models/todo.js
import { ObservableArray, ObservableObject, type } from "can";
export default class Todo extends ObservableObject {
static props = {
id: { type: type.convert(String), identity: true },
name: { type: type.convert(String) },
complete: {
type: type.maybeConvert(Boolean),
default: false
}
};
toggleComplete() {
this.complete = !this.complete;
}
}
export class TodoList extends ObservableArray {
static items = type.convert(Todo);
static props = {
get active() {
return this.filter({
complete: false
});
},
get complete() {
return this.filter({
complete: true
});
},
get allComplete() {
return this.length === this.complete.length;
}
};
}
Simulate the service layer (can-fixture)
The problem
Simulate a service layer that handles the following requests and responses:
GET /api/todos
-> GET /api/todos
<- {
"data": [
{ "name": "mow lawn", "complete": false, "id": 5 },
{ "name": "dishes", "complete": true, "id": 6 },
{ "name": "learn canjs", "complete": false, "id": 7 }
]
}
This should also support a sort
and complete
params like:
-> GET /api/todos?sort=name&complete=true
GET /api/todos/{id}
-> GET /api/todos/5
<- { "name": "mow lawn", "complete": false, "id": 5 }
POST /api/todos
-> POST /api/todos
{"name": "learn can-fixture", "complete": false}
<- {"id": 8}
PUT /api/todos/{id}
-> PUT /api/todos/8
{"name": "learn can-fixture", "complete": true}
<- {"id": 8, "name": "learn can-fixture", "complete": true}
DELETE /api/todos/{id}
-> DELETE /api/todos/8
<- {}
What you need to know
can-fixture - is used to trap AJAX requests like:
fixture("/api/entities", function(request) { request.data.folderId //-> "1" return {data: [ /* ... */ ]} })
can-fixture.store - can be used to automatically filter records if given a [can-set.Algebra].
const entities = [ /* ... */ ]; const entitiesStore = fixture.store( entities, entitiesAlgebra ); fixture("/api/entities/{id}", entitiesStore);
The solution
Create models/todos-fixture.js as follows:
// models/todos-fixture.js
import { fixture } from "can";
import Todo from "./todo";
const todoStore = fixture.store(
[
{ name: "mow lawn", complete: false, id: "5" },
{ name: "dishes", complete: true, id: "6" },
{ name: "learn canjs", complete: false, id: "7" }
],
Todo
);
fixture("/api/todos/{id}", todoStore);
fixture.delay = 500;
export default todoStore;
Connect the Todo model to the service layer (can-connect)
The problem
- Decorate
Todo
with methods so it can get, create, updated, and delete todos at the/api/todos
service. Specifically:Todo.getList()
which callsGET /api/todos
Todo.get({ id: 5 })
which callsGET /api/todos/5
todo.save()
which callsPOST /api/todos
iftodo
doesn’t have anid
orPUT /api/todos/{id}
if thetodo
has an id.todo.destroy()
which callsDELETE /api/todos/5
What you need to know
The can-connect Presentation up to and including Migrate 2 can-connect.
can-connect/can/base-map/base-map can decorate an ObservableObject with methods that connect it to a restful URL like:
baseMap({ ObjectType: Type, url: "URL", algebra: algebra })
The solution
Update models/todo.js to the following:
// models/todo.js
import {
ObservableArray,
ObservableObject,
type,
realtimeRestModel
} from "can";
export default class Todo extends ObservableObject {
static props = {
id: { type: type.convert(String), identity: true },
name: { type: type.convert(String) },
complete: {
type: type.maybeConvert(Boolean),
default: false
}
};
toggleComplete() {
this.complete = !this.complete;
}
}
export class TodoList extends ObservableArray {
static items = type.convert(Todo);
static props = {
get active() {
return this.filter({
complete: false
});
},
get complete() {
return this.filter({
complete: true
});
},
get allComplete() {
return this.length === this.complete.length;
}
};
};
Todo.connection = realtimeRestModel({
url: "/api/todos/{id}",
ObjectType: Todo,
ArrayType: TodoList
});
List todos from the service layer (can-connect use)
The problem
Get all todos
from the service layer using the "connected" Todo
type.
What you need to know
The can-connect Presentation up to and including Important Interfaces.
Type.getList gets data using the connection’s getList and returns a promise that resolves to the
TypeList
of instances:Type.getList({}).then(function(list) { });
An async getter property behavior can be used to "set" a property to an initial value:
property: { async(resolve) { SOME_ASYNC_METHOD(function callback(data) { resolve(data); }); } }
The solution
Update index.js to the following:
// index.js
import { StacheElement } from "can";
import view from "./index.stache";
import Todo from "~/models/todo";
import test from "can-todomvc-test";
import "~/models/todos-fixture";
class TodoMVC extends StacheElement {
static view = view;
static props = {
appName: { default: "TodoMVC" },
todosList: {
async(resolve) {
Todo.getList({}).then(resolve);
}
}
};
}
customElements.define("todo-mvc", TodoMVC);
const appVM = document.querySelector("todo-mvc");
test(appVM);
Toggling a todo’s checkbox updates service layer (can-connect use)
The problem
Update the service layer when a todo’s completed status changes. Also, disable the checkbox while the update is happening.
What you need to know
Call save to update a "connected"
Map
instance:map.save();
save()
can also be called by an on:event binding.isSaving returns true when
.save()
has been called, but has not resolved yet.map.isSaving()
The solution
Update index.stache to the following:
<!-- index.stache -->
<section id="todoapp">
<header id="header">
<h1>{{ this.appName }}</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">
{{# for(todo of this.todosList) }}
<li class="todo {{# if(todo.complete) }}completed{{/ if }}">
<div class="view">
<input
class="toggle"
type="checkbox"
checked:bind="todo.complete"
on:change="todo.save()"
disabled:from="todo.isSaving()"
>
<label>{{ todo.name }}</label>
<button class="destroy"></button>
</div>
<input class="edit" type="text" value="{{ todo.name }}">
</li>
{{/ for }}
</ul>
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{ this.todosList.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 ({{ this.todosList.complete.length }})
</button>
</footer>
</section>
Delete todos in the page (can-connect use)
The problem
When a todo’s destroy button is clicked, we need to delete the
todo on the server and remove the todo’s element from the page. While
the todo is being destroyed, add destroying
to the todo’s <li>
’s class
attribute.
What you need to know
The remaining parts of the can-connect Presentation, with an emphasis on how real-time behavior works.
Delete a record on the server with destroy like:
map.destroy()
isDestroying returns true when
.destroy()
has been called, but has not resolved yet.map.isDestroying()
The solution
Update index.stache to the following:
<!-- index.stache -->
<section id="todoapp">
<header id="header">
<h1>{{ this.appName }}</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">
{{# for(todo of this.todosList) }}
<li class="todo {{# if(todo.complete) }}completed{{/ if }}
{{# if(todo.isDestroying()) }}destroying{{/ if }}">
<div class="view">
<input
class="toggle"
type="checkbox"
checked:bind="todo.complete"
on:change="todo.save()"
disabled:from="todo.isSaving()"
>
<label>{{ todo.name }}</label>
<button class="destroy" on:click="todo.destroy()"></button>
</div>
<input class="edit" type="text" value="{{ todo.name }}">
</li>
{{/ for }}
</ul>
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{ this.todosList.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 ({{ this.todosList.complete.length }})
</button>
</footer>
</section>
Create todos (StacheElement)
The problem
Make it possible to create a todo, update the service layer and show the todo in the list of todos.
This functionality should be encapsulated by a <todo-create/>
custom element.
What you need to know
A StacheElement combines a custom tag name, can-stache view and ObservableObject-like properties:
import StacheElement from "can-stache-element"; import view from "./template.stache"; class SomeElement extends StacheElement { static view = view; static props = { ... }; } customElements.define("some-element", SomeElement);
You can use
on:enter
to listen to when the user hits the enter key.Listening to the
enter
event can be enabled by can-event-dom-enter/add-global/add-global.The get default() behavior creates a default value when an ObservableObject property is read for the first time.
import { ObservableObject } from "can/everything"; class Example extends ObservableObject { static props = { prop: { get default() { return []; } } }; } const ex = new Example(); console.log( ex.prop ); //-> []
Use can-view-import to import a module from a template like:
<can-import from="~/components/some-component/" /> <some-component>
The solution
Create components/todo-create/todo-create.js as follows:
import { StacheElement, enterEvent, domEvents } from "can";
import Todo from "~/models/todo";
domEvents.addEvent(enterEvent);
class TodoCreate extends StacheElement {
static view = `
<input
id="new-todo"
placeholder="What needs to be done?"
value:bind="this.todo.name"
on:enter="this.createTodo()"
>
`;
static props = {
todo: {
get default() {
return new Todo();
}
}
};
createTodo() {
this.todo.save().then(() => {
this.todo = new Todo();
});
}
}
customElements.define("todo-create", TodoCreate);
Update index.stache to the following:
<!-- index.stache -->
<can-import from="~/components/todo-create/" />
<section id="todoapp">
<header id="header">
<h1>{{ this.appName }}</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">
{{# for(todo of this.todosList) }}
<li class="todo {{# if(todo.complete) }}completed{{/ if }}
{{# if(todo.isDestroying()) }}destroying{{/ if }}">
<div class="view">
<input
class="toggle"
type="checkbox"
checked:bind="todo.complete"
on:change="todo.save()"
disabled:from="todo.isSaving()"
>
<label>{{ todo.name }}</label>
<button class="destroy" on:click="todo.destroy()"></button>
</div>
<input class="edit" type="text" value="{{ todo.name }}">
</li>
{{/ for }}
</ul>
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{ this.todosList.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 ({{ this.todosList.complete.length }})
</button>
</footer>
</section>
Edit todo names (can-stache-bindings)
The problem
Make it possible to edit a todos
name by
double-clicking its label which should reveal
a focused input element. If the user hits
the enter key, the todo should be updated on the
server. If the input loses focus, it should go
back to the default list view.
This functionality should be encapsulated by a <todo-list {todos} />
custom element. It should accept a todos
property that
is the list of todos that will be managed by the custom element.
What you need to know
The can-stache-bindings presentation on data bindings.
The [can-util/dom/attr/attr.special.focused] custom attribute can be used to specify when an element should be focused:
focused:from="shouldBeFocused()"
Use key:from to pass a value from the scope to a component:
<some-component nameInComponent:from="nameInScope" />
this can be used to get the current context in stache:
<div on:click="doSomethingWith(this)" />
The solution
Create components/todo-list/todo-list.stache as follows:
<!-- components/todo-list/todo-list.stache -->
<ul id="todo-list">
{{# for(todo of this.todos) }}
<li
class="todo {{# if(todo.complete) }}completed{{/ if }}
{{# if( todo.isDestroying() ) }}destroying{{/ if }}
{{# if(this.isEditing(todo)) }}editing{{/ if }}"
>
<div class="view">
<input
class="toggle"
type="checkbox"
checked:bind="todo.complete"
on:change="todo.save()"
disabled:from="todo.isSaving()"
>
<label on:dblclick="this.edit(todo)">{{ todo.name }}</label>
<button class="destroy" on:click="todo.destroy()"></button>
</div>
<input
class="edit"
type="text"
value:bind="todo.name"
on:enter="this.updateName()"
focused:from="this.isEditing(todo)"
on:blur="this.cancelEdit()"
>
</li>
{{/ for }}
</ul>
Create components/todo-list/todo-list.js as follows:
// components/todo-list/todo-list.js
import { StacheElement, type } from "can";
import view from "./todo-list.stache";
import Todo, { TodoList } from "~/models/todo";
export default class TodoListElement extends StacheElement {
static view = view;
static props = {
todos: type.convert(TodoList),
editing: type.maybeConvert(Todo),
backupName: String
};
isEditing(todo) {
return todo === this.editing;
}
edit(todo) {
this.backupName = todo.name;
this.editing = todo;
}
cancelEdit() {
if (this.editing) {
this.editing.name = this.backupName;
}
this.editing = null;
}
updateName() {
this.editing.save();
this.editing = null;
}
}
customElements.define("todo-list", TodoListElement);
Update index.stache to the following:
<!-- index.stache -->
<can-import from="~/components/todo-create/" />
<can-import from="~/components/todo-list/" />
<section id="todoapp">
<header id="header">
<h1>{{ this.appName }}</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="this.todosList" />
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{ this.todosList.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 ({{ this.todosList.complete.length }})
</button>
</footer>
</section>
Toggle all todos complete state
The problem
Make the “toggle all” checkbox work. It should be unchecked if a single todo is unchecked and checked if all todos are checked.
When the “toggle all” checkbox is changed, the application should update every todo to match the status of the “toggle all” checkbox.
The “toggle all” checkbox should be disabled if a single todo is saving.
What you need to know
Using setters and getters a virtual property can be simulated like:
class Person extends ObservableObject { static props = { first: String, last: String, get fullName() { return this.first + " " + this.last; }, set fullName(newValue) { const parts = newValue.split(" "); this.first = parts[0]; this.last = parts[1]; } }; }
The solution
Update models/todo.js to the following:
// models/todo.js
import {
ObservableArray,
ObservableObject,
type,
realtimeRestModel
} from "can";
export default class Todo extends ObservableObject {
static props = {
id: { type: type.convert(String), identity: true },
name: { type: type.convert(String) },
complete: {
type: type.maybeConvert(Boolean),
default: false
}
};
toggleComplete() {
this.complete = !this.complete;
}
}
export class TodoList extends ObservableArray {
static items = type.convert(Todo);
static props = {
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(value) {
this.forEach(function(todo) {
todo.complete = value;
todo.save();
});
}
};
Todo.connection = realtimeRestModel({
url: "/api/todos/{id}",
ObjectType: Todo,
ArrayType: TodoList
});
Update index.js to the following:
// index.js
import { StacheElement } from "can";
import view from "./index.stache";
import Todo from "~/models/todo";
import test from "can-todomvc-test";
import "~/models/todos-fixture";
class TodoMVC extends StacheElement {
static view = view;
static props = {
appName: { default: "TodoMVC" },
todosList: {
async(resolve) {
Todo.getList({}).then(resolve);
}
},
get allChecked() {
return this.todosList && this.todosList.allComplete;
},
set allChecked(newVal) {
this.todosList && this.todosList.updateCompleteTo(newVal);
}
};
}
customElements.define("todo-mvc", TodoMVC);
test(document.querySelector("todo-mvc"));
Update index.stache to the following:
<!-- index.stache -->
<can-import from="~/components/todo-create/" />
<can-import from="~/components/todo-list/" />
<section id="todoapp">
<header id="header">
<h1>{{ this.appName }}</h1>
<todo-create />
</header>
<section id="main" class="">
<input
id="toggle-all"
type="checkbox"
checked:bind="this.allChecked"
disabled:from="this.todosList.saving.length"
>
<label for="toggle-all">Mark all as complete</label>
<todo-list todos:from="this.todosList" />
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{ this.todosList.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 ({{ this.todosList.complete.length }})
</button>
</footer>
</section>
Clear completed todo’s (event bindings)
The problem
Make the "Clear completed" button work. When the button is clicked, It should destroy each completed todo.
What you need to know
The can-stache-bindings Presentation’s DOM Event Bindings
Use on:EVENT to listen to an event on an element and call a method in can-stache. For example, the following calls
doSomething()
when the<div>
is clicked.<div on:click="doSomething()"> ... </div>
The solution
Update models/todo.js to the following:
// models/todo.js
import {
ObservableArray,
ObservableObject,
type,
realtimeRestModel
} from "can";
export default class Todo extends ObservableObject {
static props = {
id: { type: type.convert(String), identity: true },
name: { type: type.convert(String) },
complete: {
type: type.maybeConvert(Boolean),
default: false
}
};
toggleComplete() {
this.complete = !this.complete;
}
}
export class TodoList extends ObservableArray {
static items = type.convert(Todo);
static props = {
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(value) {
this.forEach(function(todo) {
todo.complete = value;
todo.save();
});
}
destroyComplete() {
this.complete.forEach(function(todo) {
todo.destroy();
});
}
};
Todo.connection = realtimeRestModel({
url: "/api/todos/{id}",
ObjectType: Todo,
ArrayType: TodoList
});
Update index.stache to the following:
<!-- index.stache -->
<can-import from="~/components/todo-create/" />
<can-import from="~/components/todo-list/" />
<section id="todoapp">
<header id="header">
<h1>{{ this.appName }}</h1>
<todo-create />
</header>
<section id="main" class="">
<input
id="toggle-all"
type="checkbox"
checked:bind="this.allChecked"
disabled:from="this.todosList.saving.length"
>
<label for="toggle-all">Mark all as complete</label>
<todo-list todos:from="this.todosList" />
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{ this.todosList.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" on:click="this.todosList.destroyComplete()">
Clear completed ({{ this.todosList.complete.length }})
</button>
</footer>
</section>
Set up routing (can-route)
Make it so that the following URLs display the corresponding todos:
#!
or- All todos
#!active
- Only the incomplete todos#!complete
- Only the completed todos
Also, the All, Active, and Completed buttons should
link to those pages and a class="selected"
property should
be added if they represent the current page.
What you need to know
can-route is used to connect ObservableObject’s properties to the URL. This is done with data like:
route.data = new ObservableObject();
can-route can create pretty routing rules. For example, if
#!login
should set thepage
property of the element to"login"
, useroute.register()
like:route.register("{page}");
start initializes the connection between the URL and the main element properties. After you’ve created all your application’s pretty routing rules, call it like:
route.start()
The can-stache-route-helpers module provides helpers that use can-route.
routeCurrent(hash) returns truthy if the current route matches its first parameters properties.
{{# if(routeCurrent(page='login',true)) }} You are on the login page. {{/ if }}
routeUrl(hashes) returns a URL that will set its first parameters properties:
<a href="{{ routeUrl(page='login') }}">Login</a>
The solution
Update index.js to the following:
// index.js
import { route, StacheElement } from "can";
import view from "./index.stache";
import Todo from "~/models/todo";
import test from "can-todomvc-test";
import "~/models/todos-fixture";
route.register("{filter}");
class TodoMVC extends StacheElement {
static view = view;
static props = {
appName: { default: "TodoMVC" },
routeData: {
get default() {
route.start();
return route.data;
}
},
allTodos: {
async(resolve) {
Todo.getList({}).then(resolve);
}
},
get todosList() {
if (this.allTodos) {
if (this.routeData.filter === "complete") {
return this.allTodos.complete;
} else if (this.routeData.filter === "active") {
return this.allTodos.active;
} else {
return this.allTodos;
}
}
},
get allChecked() {
return this.todosList && this.todosList.allComplete;
},
set allChecked(newVal) {
this.todosList && this.todosList.updateCompleteTo(newVal);
}
};
}
customElements.define("todo-mvc", TodoMVC);
test(document.querySelector("todo-mvc"));
Update index.stache to the following:
<!-- index.stache -->
<can-import from="~/components/todo-create/" />
<can-import from="~/components/todo-list/" />
<can-import from="can-stache-route-helpers" />
<section id="todoapp">
<header id="header">
<h1>{{ this.appName }}</h1>
<todo-create />
</header>
<section id="main" class="">
<input
id="toggle-all"
type="checkbox"
checked:bind="this.allChecked"
disabled:from="this.todosList.saving.length"
>
<label for="toggle-all">Mark all as complete</label>
<todo-list todos:from="this.todosList" />
</section>
<footer id="footer" class="">
<span id="todo-count">
<strong>{{ this.todosList.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="this.todosList.destroyComplete()">
Clear completed ({{ this.todosList.complete.length }})
</button>
</footer>
</section>
Success! You’ve completed this guide. Have questions or comments? Join our Slack and let us know in the #canjs channel or our forums!