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 6 — 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@6/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.
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@6/index.mjs";
todoFixture(3);
import { StacheElement } from "//unpkg.com/can@6/core.mjs";
class TodosApp extends StacheElement {
static view = `
<h1>Today’s to-dos</h1>
`;
static props = {};
}
customElements.define("todos-app", TodosApp);
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@6/index.mjs";
todoFixture(3);
import { StacheElement } from "//unpkg.com/can@6/core.mjs";
class TodosApp extends StacheElement {
static view = `
<h1>Today’s to-dos</h1>
`;
static props = {};
}
customElements.define("todos-app", TodosApp);
Here’s what the different parts mean:
import
is a keyword that loads modules from files.StacheElement
is the named export from CanJS that lets us create custom element constructors.//unpkg.com/can@6/core.mjs
loads thecore.mjs
file from CanJS 6; this is explained more thoroughly in the setup guide.unpkg.com
is a CDN that hosts packages like CanJS (can).
Defining a custom element
The StacheElement
named export comes from CanJS’s can-stache-element package.
CanJS is composed of dozens of different packages that are responsible for different features. can-stache-element 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@6/index.mjs";
todoFixture(3);
import { StacheElement } from "//unpkg.com/can@6/core.mjs";
class TodosApp extends StacheElement {
static view = `
<h1>Today’s to-dos</h1>
`;
static props = {};
}
customElements.define("todos-app", TodosApp);
The StacheElement
class can be extended to define a new custom element. It has two properties:
static view
is a stache template that gets parsed by CanJS and inserted into the custom element; more on that later.static props
is an object that defines the properties available to the view.
After calling customElements.define()
with the custom element’s tag name and constructor, a new instance of the TodosApp
class will be instantiated every time
<todos-app>
is used.
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 data
A custom element’s view has access to all the properties in the props object.
Let’s update our custom element to be a little more interesting:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@6/index.mjs";
todoFixture(3);
import { StacheElement } from "//unpkg.com/can@6/core.mjs";
class TodosApp extends StacheElement {
static view = `
<h1>{{ this.title }}</h1>
`;
static props = {
get title() {
return "Today’s to-dos!";
}
};
}
customElements.define("todos-app", TodosApp);
Using this custom element 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
Each time a custom element is created, each property listed in props
will be defined on the instance.
We’ve added a title
getter
to our props
, which returns the string "Today’s to-dos!"
:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@6/index.mjs";
todoFixture(3);
import { StacheElement } from "//unpkg.com/can@6/core.mjs";
class TodosApp extends StacheElement {
static view = `
<h1>{{ this.title }}</h1>
`;
static props = {
get title() {
return "Today’s to-dos!";
}
};
}
customElements.define("todos-app", TodosApp);
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@6/index.mjs";
todoFixture(3);
import { StacheElement } from "//unpkg.com/can@6/core.mjs";
class TodosApp extends StacheElement {
static view = `
<h1>{{ this.title }}</h1>
`;
static props = {
get title() {
return "Today’s to-dos!";
}
};
}
customElements.define("todos-app", TodosApp);
this
inside a stache template refers to the custom element, so {{ this.title }}
makes stache
read the title
property on the custom element, 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 custom element’s properties and methods.
- 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@6/index.mjs";
todoFixture(3);
import { realtimeRestModel, StacheElement } from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static view = `
<h1>Today’s to-dos</h1>
`;
static props = {
get todosPromise() {
return Todo.getList({ sort: "name" });
}
};
}
customElements.define("todos-app", TodosApp);
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@6/index.mjs";
todoFixture(3);
import { realtimeRestModel, StacheElement } from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static view = `
<h1>Today’s to-dos</h1>
`;
static props = {
get todosPromise() {
return Todo.getList({ sort: "name" });
}
};
}
customElements.define("todos-app", TodosApp);
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@6/index.mjs";
todoFixture(3);
import { realtimeRestModel, StacheElement } from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static view = `
<h1>Today’s to-dos</h1>
`;
static props = {
get todosPromise() {
return Todo.getList({ sort: "name" });
}
};
}
customElements.define("todos-app", TodosApp);
/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 .ObjectType
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 custom element:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@6/index.mjs";
todoFixture(3);
import { realtimeRestModel, StacheElement } from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static view = `
<h1>Today’s to-dos</h1>
`;
static props = {
get todosPromise() {
return Todo.getList({ sort: "name" });
}
};
}
customElements.define("todos-app", TodosApp);
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 custom element!
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@6/index.mjs";
todoFixture(3);
import { realtimeRestModel, StacheElement } from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static 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 }}
`;
static props = {
get todosPromise() {
return Todo.getList({ sort: "name" });
}
};
}
customElements.define("todos-app", TodosApp);
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@6/index.mjs";
todoFixture(3);
import { realtimeRestModel, StacheElement } from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static 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 }}
`;
static props = {
get todosPromise() {
return Todo.getList({ sort: "name" });
}
};
}
customElements.define("todos-app", TodosApp);
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@6/index.mjs";
todoFixture(3);
import { realtimeRestModel, StacheElement } from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static 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 }}
`;
static props = {
newName: String,
get todosPromise() {
return Todo.getList({ sort: "name" });
}
};
save() {
const todo = new Todo({ name: this.newName });
todo.save();
this.newName = "";
}
}
customElements.define("todos-app", TodosApp);
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 element)
- <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 element)
Let’s examine our code more closely:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@6/index.mjs";
todoFixture(3);
import { realtimeRestModel, StacheElement } from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static 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 }}
`;
static props = {
newName: String,
get todosPromise() {
return Todo.getList({ sort: "name" });
}
};
save() {
const todo = new Todo({ name: this.newName });
todo.save();
this.newName = "";
}
}
customElements.define("todos-app", TodosApp);
value:bind="this.newName"
will create a binding between the input’s value
property and
the custom element’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 custom element… 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@6/index.mjs";
todoFixture(3);
import { realtimeRestModel, StacheElement } from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static 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 }}
`;
static props = {
newName: String,
get todosPromise() {
return Todo.getList({ sort: "name" });
}
};
save() {
const todo = new Todo({ name: this.newName });
todo.save();
this.newName = "";
}
}
customElements.define("todos-app", TodosApp);
When the button emits a click
event, the save()
method on the custom element will be called.
Again, you might be wondering where we’ve defined the save()
method in the custom element… we’ll get there in just a moment. 😊
Defining custom properties
Earlier we said that:
static props
is an object that defines the properties available to the view.
This is true, although there’s more information to be known. The static props
object is made up of ObservableObject-like
property definitions that explicitly configure how a custom element’s properties are defined.
We’ve been defining properties and methods on the custom element with the standard JavaScript getter and method syntax.
Now we’re going to use ObservableObject’s constructor syntax to define a property as a String:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@6/index.mjs";
todoFixture(3);
import { realtimeRestModel, StacheElement } from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static 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 }}
`;
static props = {
newName: String,
get todosPromise() {
return Todo.getList({ sort: "name" });
}
};
save() {
const todo = new Todo({ name: this.newName });
todo.save();
this.newName = "";
}
}
customElements.define("todos-app", TodosApp);
In the code above, we define a new newName
property on the custom element as a String.
If this property is set to a value that’s not a String
, CanJS will throw an error.
If you instead want the value to be converted to a string, you could use type.convert(String).
You can specify any built-in types that you want, including Boolean, Date, and Number.
Saving new items to the backend API
Now let’s look at the save()
method on our custom element:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@6/index.mjs";
todoFixture(3);
import { realtimeRestModel, StacheElement } from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static 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 }}
`;
static props = {
newName: String,
get todosPromise() {
return Todo.getList({ sort: "name" });
}
};
save() {
const todo = new Todo({ name: this.newName });
todo.save();
this.newName = "";
}
}
customElements.define("todos-app", TodosApp);
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
custom element. This is how we can both read and write the custom element’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@6/index.mjs";
todoFixture(3);
import {
realtimeRestModel,
StacheElement,
type
} from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static 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 }}
`;
static props = {
newName: String,
selected: type.maybe(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;
}
}
customElements.define("todos-app", TodosApp);
The next five sections will more thoroughly explain the code above.
Importing type
First, we import the type module:
// Creates a mock backend with 3 todos
import { todoFixture } from "//unpkg.com/can-demo-models@6/index.mjs";
todoFixture(3);
import {
realtimeRestModel,
StacheElement,
type
} from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static 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 }}
`;
static props = {
newName: String,
selected: type.maybe(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;
}
}
customElements.define("todos-app", TodosApp);
This module gives us helpers for type checking and conversion.
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@6/index.mjs";
todoFixture(3);
import {
realtimeRestModel,
StacheElement,
type
} from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static 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 }}
`;
static props = {
newName: String,
selected: type.maybe(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;
}
}
customElements.define("todos-app", TodosApp);
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@6/index.mjs";
todoFixture(3);
import {
realtimeRestModel,
StacheElement,
type
} from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static 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 }}
`;
static props = {
newName: String,
selected: type.maybe(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;
}
}
customElements.define("todos-app", TodosApp);
The code above checks whether todo
is equal to this.selected
. We haven’t added selected
to our custom element 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@6/index.mjs";
todoFixture(3);
import {
realtimeRestModel,
StacheElement,
type
} from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static 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 }}
`;
static props = {
newName: String,
selected: type.maybe(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;
}
}
customElements.define("todos-app", TodosApp);
on:click="this.selected = todo"
will cause the custom element’s selected
property to be set
to the todo
when the <span>
is clicked.
Additionally, we add selected: type.maybe(Todo)
to the custom element.
This allows us to set selected
to either an instance of Todo
or null
.
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@6/index.mjs";
todoFixture(3);
import {
realtimeRestModel,
StacheElement,
type
} from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static 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 }}
`;
static props = {
newName: String,
selected: type.maybe(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;
}
}
customElements.define("todos-app", TodosApp);
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 custom element’ssaveTodo()
method is calledvalue:bind="todo.name"
binds the input’s value to thename
property on thetodo
saveTodo(todo)
in the custom element will callsave()
on thetodo
and reset the custom element’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@6/index.mjs";
todoFixture(3);
import {
realtimeRestModel,
StacheElement,
type
} from "//unpkg.com/can@6/core.mjs";
const Todo = realtimeRestModel("/api/todos/{id}").ObjectType;
class TodosApp extends StacheElement {
static 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 }}
`;
static props = {
newName: String,
selected: type.maybe(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;
}
}
customElements.define("todos-app", TodosApp);
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 6 — 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!