CRUD Guide
Learn how to build a basic CRUD app with CanJS in 30 minutes.
Overview
In this tutorial, we’ll build a simple to-do app that lets you:
- Load a list of to-dos from an API
- Create new to-dos with a form
- Mark to-dos as “completed”
- Delete to-dos
See the Pen CanJS 5 — Basic Todo App by Bitovi (@bitovi) on CodePen.
This tutorial does not assume any prior knowledge of CanJS and is meant for complete beginners. We assume that you have have basic knowledge of HTML and JavaScript. If you don’t, start by going through MDN’s tutorials.
Setup
We’ll use CodePen in this tutorial to edit code in our browser and immediately see the results. If you’re feeling adventurous and you’d like to set up the code locally, the setup guide has all the info you’ll need.
To begin, click the “Edit on CodePen” button in the top right of the following embed:
See the Pen CanJS 5 — CRUD Guide Step 1 by Bitovi (@bitovi) on CodePen.
The next two sections will explain what’s already in the HTML and JS tabs in the CodePen.
HTML
The CodePen above has one line of HTML already in it:
<todos-app></todos-app>
<todos-app>
is a custom element.
When the browser encounters this element, it looks for the todos-app
element to be defined in JavaScript.
In just a little bit, we’ll define the todos-app
element with CanJS.
JS
The CodePen above has three lines of JavaScript already in it:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
Instead of connecting to a real backend API or web service, we’ll use fixtures to “mock” an API. Whenever an AJAX request is made, the fixture will “capture” the request and instead respond with mock data.
Note: if you open your browser’s Network panel, you will not see any network requests. You can see the fixture requests and responses in your browser’s Console panel.
How fixtures work is outside the scope of this tutorial and not necessary to understand to continue, but you can learn more in the can-fixture documentation.
Defining a custom element with CanJS
We mentioned above that CanJS helps you define custom elements. We call these components.
Add the following to the JS tab in your CodePen:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component } from "//unpkg.com/can@5/core.mjs";
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
`,
ViewModel: {
}
});
After you add the above code, you’ll see “Today’s to-dos” displayed in the result pane.
We’ll break down what each of these lines does in the next couple sections.
Importing CanJS
With one line of code, we load CanJS from a CDN and import one of its modules:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component } from "//unpkg.com/can@5/core.mjs";
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
`,
ViewModel: {
}
});
Here’s what the different parts mean:
import
is a keyword that loads modules from files.Component
is the named export from CanJS that lets us define custom elements.//unpkg.com/can@5/core.mjs
loads thecore.mjs
file from CanJS 5; this is explained more thoroughly in the setup guide.unpkg.com
is a CDN that hosts packages like CanJS (can).
Defining a component
The Component
named export comes from CanJS’s can-component package.
CanJS is composed of dozens of different packages that are responsible for different features. can-component is responsible for letting us define custom elements that can be used by the browser.
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component } from "//unpkg.com/can@5/core.mjs";
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
`,
ViewModel: {
}
});
Calling Component.extend() defines a custom element. It takes three arguments:
tag
is the name of the custom element.view
is a stache template that gets parsed by CanJS and inserted into the custom element; more on that later.ViewModel
is an object (with properties and methods) from which the view gets its model data.
The view
is pretty boring right now; it just renders <h1>Today’s to-dos</h1>
. In the next section, we’ll make it more interesting!
Find something confusing or need help? Join our Slack and post a question in the #canjs channel. We answer every question and we’re eager to help!
Rendering a template with a ViewModel
A component’s view gets rendered with a ViewModel.
Let’s update our component to be a little more interesting:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component } from "//unpkg.com/can@5/core.mjs";
Component.extend({
tag: "todos-app",
view: `
<h1>{{ this.title }}</h1>
`,
ViewModel: {
get title() {
return "Today’s to-dos!";
}
}
});
Using this component will insert the following into the page:
<todos-app>
<h1>Today’s to-dos!</h1>
</todos-app>
The next two sections will explain these lines.
Defining properties on the ViewModel
Every time a component’s custom element is used, a new instance of the component’s ViewModel
is created.
We’ve added a title
getter
to our ViewModel
, which returns the string "Today’s to-dos!"
:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component } from "//unpkg.com/can@5/core.mjs";
Component.extend({
tag: "todos-app",
view: `
<h1>{{ this.title }}</h1>
`,
ViewModel: {
get title() {
return "Today’s to-dos!";
}
}
});
Reading properties in the stache template
Our view
is a stache template. Whenever stache encounters the double curlies ({{ }}
),
it looks inside them for an expression to evaluate.
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component } from "//unpkg.com/can@5/core.mjs";
Component.extend({
tag: "todos-app",
view: `
<h1>{{ this.title }}</h1>
`,
ViewModel: {
get title() {
return "Today’s to-dos!";
}
}
});
this
inside a stache template refers to the ViewModel instance created for that component, so {{ this.title }}
makes stache
read the title
property on the component’s ViewModel instance, which is how <h1>Today’s to-dos!</h1>
gets rendered in the page!
Find something confusing or need help? Join our Slack and post a question in the #canjs channel. We answer every question and we’re eager to help!
Connecting to a backend API
With most frameworks, you might use XMLHttpRequest, fetch, or a third-party library to make HTTP requests.
CanJS provides abstractions for connecting to backend APIs so you can:
- Use a standard interface for creating, retrieving, updating, and deleting data.
- Avoid writing the requests yourself.
- Convert raw data from the server to typed data, with properties and methods, just like a ViewModel.
- Have your UI update whenever the model data changes.
- Prevent multiple instances of a given object or multiple lists of a given set from being created.
In our app, let’s make a request to get all the to-dos sorted alphabetically by name. Note that we won’t see any to-dos in our app yet; we’ll get to that in just a little bit!
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
`,
ViewModel: {
get todosPromise() {
return Todo.getList({sort: "name"});
}
}
});
The next three sections will explain these lines.
Importing realtimeRestModel
First, we import realtimeRestModel:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
`,
ViewModel: {
get todosPromise() {
return Todo.getList({sort: "name"});
}
}
});
This module is responsible for creating new connections to APIs and new models (data types).
Creating a new model
Second, we call realtimeRestModel()
with a string that represents the URLs that should be called for
creating, retrieving, updating, and deleting data:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
`,
ViewModel: {
get todosPromise() {
return Todo.getList({sort: "name"});
}
}
});
/api/todos/{id}
will map to these API calls:
GET /api/todos
to retrieve all the to-dosPOST /api/todos
to create a to-doGET /api/todos/1
to retrieve the to-do withid=1
PUT /api/todos/1
to update the to-do withid=1
DELETE /api/todos/1
to delete the to-do withid=1
realtimeRestModel()
returns what we call a connection. It’s just an object that has a .Map
property.
The Todo
is a new model that has these methods for making API calls:
Todo.getList()
callsGET /api/todos
new Todo().save()
callsPOST /api/todos
Todo.get({id: 1})
callsGET /api/todos/1
Additionally, once you have an instance of a todo
, you can call these methods on it:
todo.save()
callsPUT /api/todos/1
todo.destroy()
callsDELETE /api/todos/1
Note: the Data Modeling section in the API Docs has a cheat sheet with each JavaScript call, the HTTP request that’s made, and the expected JSON response.
Fetching all the to-dos
Third, we add a new getter to our ViewModel:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
`,
ViewModel: {
get todosPromise() {
return Todo.getList({sort: "name"});
}
}
});
Todo.getList({sort: "name"})
will make a GET
request to /api/todos?sort=name
.
It returns a Promise
that resolves with the data returned by the API.
Find something confusing or need help? Join our Slack and post a question in the #canjs channel. We answer every question and we’re eager to help!
Rendering a list of items
Now that we’ve learned how to fetch data from an API, let’s render the data in our component!
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
{{# if(this.todosPromise.isResolved) }}
<ul>
{{# for(todo of this.todosPromise.value) }}
<li class="{{# if(todo.complete) }}done{{/ if }}">
{{ todo.name }}
</li>
{{/ for }}
</ul>
{{/ if }}
`,
ViewModel: {
get todosPromise() {
return Todo.getList({sort: "name"});
}
}
});
This template uses two stache helpers:
- #if() checks whether the result of the expression inside is truthy.
- #for(of) loops through an array of values.
This template also shows how we can read the state and value of a Promise:
.isResolved
returnstrue
when the Promise resolves with a value.value
returns the value with which the Promise was resolved
So first, we check #if(this.todosPromise.isResolved)
is true. If it is, we loop through all
the to-dos (#for(todo of this.todosPromise.value)
) and create a todo
variable in our template.
Then we read {{ todo.name }}
to put the to-do’s name in the list. Additionally, the li’s class
changes depending on if todo.complete
is true or false.
Handling loading and error states
Now let’s also:
- Show “Loading…” when the to-dos list loading
- Show a message if there’s an error loading the to-dos
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
{{# if(this.todosPromise.isPending) }}
Loading todos…
{{/ if }}
{{# if(this.todosPromise.isRejected) }}
<p>Couldn’t load todos; {{ this.todosPromise.reason }}</p>
{{/ if }}
{{# if(this.todosPromise.isResolved) }}
<ul>
{{# for(todo of this.todosPromise.value) }}
<li class="{{# if(todo.complete) }}done{{/ if }}">
{{ todo.name }}
</li>
{{/ for }}
</ul>
{{/ if }}
`,
ViewModel: {
get todosPromise() {
return Todo.getList({sort: "name"});
}
}
});
This template shows how to read more state and an error from a Promise:
.isPending
returnstrue
when the Promise has neither been resolved nor rejected.isRejected
returnstrue
when the Promise is rejected with an error.reason
returns the error with which the Promise was rejected
isPending
, isRejected
, and isResolved
are all mutually-exclusive; only one of them will be true
at any given time. The Promise will always start off as isPending
, and then either change to isRejected
if the request fails or isResolved
if it succeeds.
Creating new items
CanJS makes it easy to create new instances of your model objects and save them to your backend API.
In this section, we’ll add an <input>
for new to-do names and a button for saving new to-dos.
After a new to-do is created, we’ll reset the input so a new to-do’s name can be entered.
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
{{# if(this.todosPromise.isPending) }}
Loading todos…
{{/ if }}
{{# if(this.todosPromise.isRejected) }}
<p>Couldn’t load todos; {{ this.todosPromise.reason }}</p>
{{/ if }}
{{# if(this.todosPromise.isResolved) }}
<input placeholder="What needs to be done?" value:bind="this.newName" />
<button on:click="this.save()" type="button">Add</button>
<ul>
{{# for(todo of this.todosPromise.value) }}
<li class="{{# if(todo.complete) }}done{{/ if }}">
{{ todo.name }}
</li>
{{/ for }}
</ul>
{{/ if }}
`,
ViewModel: {
newName: "string",
get todosPromise() {
return Todo.getList({sort: "name"});
},
save() {
const todo = new Todo({name: this.newName});
todo.save();
this.newName = "";
}
}
});
The next four sections will explain these lines.
Binding to input form elements
CanJS has one-way and two-way bindings in the form of:
- <child-element property:bind="key"> (two-way binding a property on child element and parent ViewModel)
- <child-element property:from="key"> (one-way binding to a child element’s property)
- <child-element property:to="key"> (one-way binding to the parent ViewModel)
Let’s examine our code more closely:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
{{# if(this.todosPromise.isPending) }}
Loading todos…
{{/ if }}
{{# if(this.todosPromise.isRejected) }}
<p>Couldn’t load todos; {{ this.todosPromise.reason }}</p>
{{/ if }}
{{# if(this.todosPromise.isResolved) }}
<input placeholder="What needs to be done?" value:bind="this.newName" />
<button on:click="this.save()" type="button">Add</button>
<ul>
{{# for(todo of this.todosPromise.value) }}
<li class="{{# if(todo.complete) }}done{{/ if }}">
{{ todo.name }}
</li>
{{/ for }}
</ul>
{{/ if }}
`,
ViewModel: {
newName: "string",
get todosPromise() {
return Todo.getList({sort: "name"});
},
save() {
const todo = new Todo({name: this.newName});
todo.save();
this.newName = "";
}
}
});
value:bind="this.newName"
will create a binding between the input’s value
property and
the ViewModel’s newName
property. When one of them changes, the other will be updated.
If you’re wondering where we’ve defined the newName
in the ViewModel… we’ll get there in just a moment. 😊
Listening for events
You can listen for events with the <child-element on:event="method()"> syntax.
Let’s look at our code again:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
{{# if(this.todosPromise.isPending) }}
Loading todos…
{{/ if }}
{{# if(this.todosPromise.isRejected) }}
<p>Couldn’t load todos; {{ this.todosPromise.reason }}</p>
{{/ if }}
{{# if(this.todosPromise.isResolved) }}
<input placeholder="What needs to be done?" value:bind="this.newName" />
<button on:click="this.save()" type="button">Add</button>
<ul>
{{# for(todo of this.todosPromise.value) }}
<li class="{{# if(todo.complete) }}done{{/ if }}">
{{ todo.name }}
</li>
{{/ for }}
</ul>
{{/ if }}
`,
ViewModel: {
newName: "string",
get todosPromise() {
return Todo.getList({sort: "name"});
},
save() {
const todo = new Todo({name: this.newName});
todo.save();
this.newName = "";
}
}
});
When the button emits a click
event, the save()
method on the ViewModel will be called.
Again, you might be wondering where we’ve defined the save()
method in the ViewModel… we’ll get there in just a moment. 😊
Defining custom properties
Earlier we said that a:
ViewModel
is an object (with properties and methods) from which the view gets its model data.
This is true, although there’s more information to be known. A component’s ViewModel is actually an instance of DefineMap, which is an observable data type used throughout CanJS.
We’ve been defining properties and methods on the ViewModel with the standard JavaScript getter and method syntax. Now we’re going to use DefineMap’s string syntax to define a property as a string:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
{{# if(this.todosPromise.isPending) }}
Loading todos…
{{/ if }}
{{# if(this.todosPromise.isRejected) }}
<p>Couldn’t load todos; {{ this.todosPromise.reason }}</p>
{{/ if }}
{{# if(this.todosPromise.isResolved) }}
<input placeholder="What needs to be done?" value:bind="this.newName" />
<button on:click="this.save()" type="button">Add</button>
<ul>
{{# for(todo of this.todosPromise.value) }}
<li class="{{# if(todo.complete) }}done{{/ if }}">
{{ todo.name }}
</li>
{{/ for }}
</ul>
{{/ if }}
`,
ViewModel: {
newName: "string",
get todosPromise() {
return Todo.getList({sort: "name"});
},
save() {
const todo = new Todo({name: this.newName});
todo.save();
this.newName = "";
}
}
});
In the code above, we define a new newName
property on the ViewModel. When this property is set,
if the new value is not null
or undefined
, CanJS will convert the new value into a string.
CanJS supports many different types, including boolean
, date
, number
, and more. You can find the
full list of types here.
Saving new items to the backend API
Now let’s look at the save()
method on our ViewModel:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
{{# if(this.todosPromise.isPending) }}
Loading todos…
{{/ if }}
{{# if(this.todosPromise.isRejected) }}
<p>Couldn’t load todos; {{ this.todosPromise.reason }}</p>
{{/ if }}
{{# if(this.todosPromise.isResolved) }}
<input placeholder="What needs to be done?" value:bind="this.newName" />
<button on:click="this.save()" type="button">Add</button>
<ul>
{{# for(todo of this.todosPromise.value) }}
<li class="{{# if(todo.complete) }}done{{/ if }}">
{{ todo.name }}
</li>
{{/ for }}
</ul>
{{/ if }}
`,
ViewModel: {
newName: "string",
get todosPromise() {
return Todo.getList({sort: "name"});
},
save() {
const todo = new Todo({name: this.newName});
todo.save();
this.newName = "";
}
}
});
This code does three things:
- Creates a new to-do with the name typed into the
<input>
(const todo = new Todo({name: this.newName})
). - Saves the new to-do to the backend API (
todo.save()
). - Resets the
<input>
so a new to-do name can be typed in (this.newName = ""
).
You’ll notice that just like within the stache template, this
inside the save()
method refers to the
component’s ViewModel instance. This is how we can both read and write the ViewModel’s newName
property.
New items are added to the right place in the sorted list
When Todo.getList({sort: "name"})
is called, CanJS makes a GET request to /api/todos?sort=name
.
When the array of to-dos comes back, CanJS associates that array with the query {sort: "name"}
.
When new to-dos are created, they’re automatically added to the right spot in the list that’s returned.
Try adding a to-do in your CodePen! You don’t have to write any code to make sure the new to-do gets inserted into the right spot in the list.
CanJS does this for filtering as well. If you make a query with a filter (e.g. {filter: {complete: true }}
),
when items are added, edited, or deleted that match that filter, those lists will be updated automatically.
Find something confusing or need help? Join our Slack and post a question in the #canjs channel. We answer every question and we’re eager to help!
Updating existing items
CanJS also makes it easy to update existing instances of your model objects and save them to your backend API.
In this section, we’ll add an <input type="checkbox">
for marking a to-do as complete.
We’ll also make it possible to click on a to-do to select it and edit its name.
After either of these changes, we’ll save the to-do to the backend API.
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
{{# if(this.todosPromise.isPending) }}
Loading todos…
{{/ if }}
{{# if(this.todosPromise.isRejected) }}
<p>Couldn’t load todos; {{ this.todosPromise.reason }}</p>
{{/ if }}
{{# if(this.todosPromise.isResolved) }}
<input placeholder="What needs to be done?" value:bind="this.newName" />
<button on:click="this.save()" type="button">Add</button>
<ul>
{{# for(todo of this.todosPromise.value) }}
<li class="{{# if(todo.complete) }}done{{/ if }}">
<label>
<input checked:bind="todo.complete" on:change="todo.save()" type="checkbox" />
</label>
{{# eq(todo, this.selected) }}
<input focused:from="true" on:blur="this.saveTodo(todo)" value:bind="todo.name" />
{{ else }}
<span on:click="this.selected = todo">
{{ todo.name }}
</span>
{{/ eq }}
</li>
{{/ for }}
</ul>
{{/ if }}
`,
ViewModel: {
newName: "string",
selected: Todo,
get todosPromise() {
return Todo.getList({sort: "name"});
},
save() {
const todo = new Todo({name: this.newName});
todo.save();
this.newName = "";
},
saveTodo(todo) {
todo.save();
this.selected = null;
}
}
});
The next four sections will more thoroughly explain the code above.
Binding to checkbox form elements
Every <input type="checkbox">
has a checked
property. We bind to it so if todo.complete
is true or false,
the checkbox is either checked or unchecked, respectively.
Additionally, when the checkbox is clicked, todo.complete
is updated to be true
or false
.
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
{{# if(this.todosPromise.isPending) }}
Loading todos…
{{/ if }}
{{# if(this.todosPromise.isRejected) }}
<p>Couldn’t load todos; {{ this.todosPromise.reason }}</p>
{{/ if }}
{{# if(this.todosPromise.isResolved) }}
<input placeholder="What needs to be done?" value:bind="this.newName" />
<button on:click="this.save()" type="button">Add</button>
<ul>
{{# for(todo of this.todosPromise.value) }}
<li class="{{# if(todo.complete) }}done{{/ if }}">
<label>
<input checked:bind="todo.complete" on:change="todo.save()" type="checkbox" />
</label>
{{# eq(todo, this.selected) }}
<input focused:from="true" on:blur="this.saveTodo(todo)" value:bind="todo.name" />
{{ else }}
<span on:click="this.selected = todo">
{{ todo.name }}
</span>
{{/ eq }}
</li>
{{/ for }}
</ul>
{{/ if }}
`,
ViewModel: {
newName: "string",
selected: Todo,
get todosPromise() {
return Todo.getList({sort: "name"});
},
save() {
const todo = new Todo({name: this.newName});
todo.save();
this.newName = "";
},
saveTodo(todo) {
todo.save();
this.selected = null;
}
}
});
We also listen for change events with the
on:event syntax. When the input’s value changes, the
save()
method on the todo
is called.
Checking for equality in templates
This section uses two stache helpers:
- #eq() checks whether all the arguments passed to it are
===
- {{ else }} will only render if
#eq()
returnsfalse
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
{{# if(this.todosPromise.isPending) }}
Loading todos…
{{/ if }}
{{# if(this.todosPromise.isRejected) }}
<p>Couldn’t load todos; {{ this.todosPromise.reason }}</p>
{{/ if }}
{{# if(this.todosPromise.isResolved) }}
<input placeholder="What needs to be done?" value:bind="this.newName" />
<button on:click="this.save()" type="button">Add</button>
<ul>
{{# for(todo of this.todosPromise.value) }}
<li class="{{# if(todo.complete) }}done{{/ if }}">
<label>
<input checked:bind="todo.complete" on:change="todo.save()" type="checkbox" />
</label>
{{# eq(todo, this.selected) }}
<input focused:from="true" on:blur="this.saveTodo(todo)" value:bind="todo.name" />
{{ else }}
<span on:click="this.selected = todo">
{{ todo.name }}
</span>
{{/ eq }}
</li>
{{/ for }}
</ul>
{{/ if }}
`,
ViewModel: {
newName: "string",
selected: Todo,
get todosPromise() {
return Todo.getList({sort: "name"});
},
save() {
const todo = new Todo({name: this.newName});
todo.save();
this.newName = "";
},
saveTodo(todo) {
todo.save();
this.selected = null;
}
}
});
The code above checks whether todo
is equal to this.selected
. We haven’t added selected
to our ViewModel yet, but we will in the next section!
Setting the selected to-do
When you listen for events with the on:event syntax, you can also set property values.
Let’s examine this part of the code:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
{{# if(this.todosPromise.isPending) }}
Loading todos…
{{/ if }}
{{# if(this.todosPromise.isRejected) }}
<p>Couldn’t load todos; {{ this.todosPromise.reason }}</p>
{{/ if }}
{{# if(this.todosPromise.isResolved) }}
<input placeholder="What needs to be done?" value:bind="this.newName" />
<button on:click="this.save()" type="button">Add</button>
<ul>
{{# for(todo of this.todosPromise.value) }}
<li class="{{# if(todo.complete) }}done{{/ if }}">
<label>
<input checked:bind="todo.complete" on:change="todo.save()" type="checkbox" />
</label>
{{# eq(todo, this.selected) }}
<input focused:from="true" on:blur="this.saveTodo(todo)" value:bind="todo.name" />
{{ else }}
<span on:click="this.selected = todo">
{{ todo.name }}
</span>
{{/ eq }}
</li>
{{/ for }}
</ul>
{{/ if }}
`,
ViewModel: {
newName: "string",
selected: Todo,
get todosPromise() {
return Todo.getList({sort: "name"});
},
save() {
const todo = new Todo({name: this.newName});
todo.save();
this.newName = "";
},
saveTodo(todo) {
todo.save();
this.selected = null;
}
}
});
on:click="this.selected = todo"
will cause the ViewModel’s selected
property to be set
to the todo
when the <span>
is clicked.
Additionally, we add selected: Todo
to the ViewModel. In our app, we only ever set selected
to an instance of a Todo
,
but if we were to set it to a plain object, a new Todo
instance would be created with that object.
Editing to-do names
After you click on a to-do’s name, we want the <span>
to be replaced with an <input>
that has the
to-do’s name (and immediately give it focus). When the input loses focus, we want the to-do to be saved
and the input to be replaced with the span again.
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
{{# if(this.todosPromise.isPending) }}
Loading todos…
{{/ if }}
{{# if(this.todosPromise.isRejected) }}
<p>Couldn’t load todos; {{ this.todosPromise.reason }}</p>
{{/ if }}
{{# if(this.todosPromise.isResolved) }}
<input placeholder="What needs to be done?" value:bind="this.newName" />
<button on:click="this.save()" type="button">Add</button>
<ul>
{{# for(todo of this.todosPromise.value) }}
<li class="{{# if(todo.complete) }}done{{/ if }}">
<label>
<input checked:bind="todo.complete" on:change="todo.save()" type="checkbox" />
</label>
{{# eq(todo, this.selected) }}
<input focused:from="true" on:blur="this.saveTodo(todo)" value:bind="todo.name" />
{{ else }}
<span on:click="this.selected = todo">
{{ todo.name }}
</span>
{{/ eq }}
</li>
{{/ for }}
</ul>
{{/ if }}
`,
ViewModel: {
newName: "string",
selected: Todo,
get todosPromise() {
return Todo.getList({sort: "name"});
},
save() {
const todo = new Todo({name: this.newName});
todo.save();
this.newName = "";
},
saveTodo(todo) {
todo.save();
this.selected = null;
}
}
});
Let’s break down the code above:
focused:from="true"
will set the input’sfocused
attribute totrue
, immediately giving the input focuson:blur="this.saveTodo(todo)"
listens for the blur event (the input losing focus) so the ViewModel’ssaveTodo()
method is calledvalue:bind="todo.name"
binds the input’s value to thename
property on thetodo
saveTodo(todo)
in the ViewModel will callsave()
on thetodo
and reset the ViewModel’sselected
property (so the input will disappear and just the to-do’s name is displayed)
Find something confusing or need help? Join our Slack and post a question in the #canjs channel. We answer every question and we’re eager to help!
Deleting items
Now there’s just one more feature we want to add to our app: deleting to-dos!
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@5/index.mjs";
todoFixture(3);
import { Component, realtimeRestModel } from "//unpkg.com/can@5/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").Map;
Component.extend({
tag: "todos-app",
view: `
<h1>Today’s to-dos</h1>
{{# if(this.todosPromise.isPending) }}
Loading todos…
{{/ if }}
{{# if(this.todosPromise.isRejected) }}
<p>Couldn’t load todos; {{ this.todosPromise.reason }}</p>
{{/ if }}
{{# if(this.todosPromise.isResolved) }}
<input placeholder="What needs to be done?" value:bind="this.newName" />
<button on:click="this.save()" type="button">Add</button>
<ul>
{{# for(todo of this.todosPromise.value) }}
<li class="{{# if(todo.complete) }}done{{/ if }}">
<label>
<input checked:bind="todo.complete" on:change="todo.save()" type="checkbox" />
</label>
{{# eq(todo, this.selected) }}
<input focused:from="true" on:blur="this.saveTodo(todo)" value:bind="todo.name" />
{{ else }}
<span on:click="this.selected = todo">
{{ todo.name }}
</span>
{{/ eq }}
<button on:click="todo.destroy()" type="button"></button>
</li>
{{/ for }}
</ul>
{{/ if }}
`,
ViewModel: {
newName: "string",
selected: Todo,
get todosPromise() {
return Todo.getList({sort: "name"});
},
save() {
const todo = new Todo({name: this.newName});
todo.save();
this.newName = "";
},
saveTodo(todo) {
todo.save();
this.selected = null;
}
}
});
When the <button>
is clicked, the to-do’s destroy
method is called, which will make a DELETE /api/todos/{id}
call to delete the to-do in the
backend API.
Result
Congrats! You’ve built your first app with CanJS and learned all the basics.
Here’s what your finished CodePen will look like:
See the Pen CanJS 5 — Basic Todo App by Bitovi (@bitovi) on CodePen.
Next steps
If you’re ready to go through another guide, check out the Chat Guide, which will walk you through building a real-time chat app. The TodoMVC Guide is also another great guide to go through if you’re not sick of building to-do apps. ☑️
If you’d rather learn about CanJS’s core technologies, the Technology Overview shows you the basics of how CanJS works. From there, the HTML, Routing, and Service Layer guides offer more in-depth information on how CanJS works.
If you haven’t already, join our Slack and come say hello in the #introductions channel. We also have a #canjs channel for any comments or questions about CanJS. We answer every question and we’re eager to help!