DoneJS StealJS jQuery++ FuncUnit DocumentJS
6.0.1
5.33.2 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
      • Migrating to CanJS 6
      • 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 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@pre/core.mjs loads the core.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-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 .ObjectType 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 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 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@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 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@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:

  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 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() returns 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);

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’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 custom element’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 custom element will call save() on the todo and reset the custom element’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@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!

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

On this page

Get help

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