DoneJS StealJS jQuery++ FuncUnit DocumentJS
5.33.3
6.0.0 4.3.0 3.14.1 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • getting started
      • CRUD Guide
      • Setting Up CanJS
      • Technology Overview
    • topics
      • HTML
      • Routing
      • Service Layer
      • Debugging
      • Forms
      • Testing
      • Logic
      • Server-Side Rendering
    • app guides
      • Chat Guide
      • TodoMVC Guide
      • TodoMVC with StealJS
    • beginner recipes
      • Canvas Clock
      • Credit Card
      • File Navigator
      • Signup and Login
      • Video Player
    • intermediate recipes
      • CTA Bus Map
      • Multiple Modals
      • Text Editor
      • Tinder Carousel
    • advanced recipes
      • Credit Card
      • File Navigator
      • Playlist Editor
      • Search, List, Details
    • upgrade
      • Migrating to CanJS 3
      • Migrating to CanJS 4
      • Migrating to CanJS 5
      • Using Codemods
    • other
      • Reading the API Docs
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

CRUD Guide

  • Edit on GitHub

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 the core.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-dos
  • POST /api/todos to create a to-do
  • GET /api/todos/1 to retrieve the to-do with id=1
  • PUT /api/todos/1 to update the to-do with id=1
  • DELETE /api/todos/1 to delete the to-do with id=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() calls GET /api/todos
  • new Todo().save() calls POST /api/todos
  • Todo.get({id: 1}) calls GET /api/todos/1

Additionally, once you have an instance of a todo, you can call these methods on it:

  • todo.save() calls PUT /api/todos/1
  • todo.destroy() calls DELETE /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 returns true 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 returns true when the Promise has neither been resolved nor rejected
  • .isRejected returns true 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:

  1. Creates a new to-do with the name typed into the <input> (const todo = new Todo({name: this.newName})).
  2. Saves the new to-do to the backend API (todo.save()).
  3. 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() returns 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;
        }
    }
});

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’s focused attribute to true, immediately giving the input focus
  • on:blur="this.saveTodo(todo)" listens for the blur event (the input losing focus) so the ViewModel’s saveTodo() method is called
  • value:bind="todo.name" binds the input’s value to the name property on the todo
  • saveTodo(todo) in the ViewModel will call save() on the todo and reset the ViewModel’s selected 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!

CanJS is part of DoneJS. Created and maintained by the core DoneJS team and Bitovi. Currently 5.33.3.

On this page

Get help

  • Chat with us
  • File an issue
  • Ask questions
  • Read latest news