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

TodoMVC Guide

  • Edit on GitHub

This guide will walk you through building a slightly modified version of TodoMVC with CanJS’s Core libraries and can-fixture. It takes about 1 hour to complete.

Setup

The easiest way to get started is to fork the following CodePen by clicking the CodePen button on the top right:

See the Pen CanJS 6.0 - TodoMVC Start by Bitovi (@bitovi) on CodePen.

The CodePen starts with the static HTML and CSS a designer might turn over to a JS developer. We will be adding all the JavaScript functionality.

Read Setting Up CanJS for instructions on alternate CanJS setups.

Define and use the main component

the problem

In this section, we will define a custom <todo-mvc> element and use it in the page’s HTML.

the solution

Replace the content of the HTML tab with the <todo-mvc> element:

<todo-mvc></todo-mvc>

Update the JavaScript tab to define the <todo-mvc> element by:

  • Extending StacheElement and registering a custom element with the tag name todo-mvc.
  • Setting the view to the html that should be displayed within the <todo-mvc> element. In this case it is the HTML that was originally in the page.
  • Instead of the hard-coded <h1>Todos</h1> title, we will read the title from the element’s properties. We'll do this by:
    • Adding magic tags like {{ this.appName }} that read appTitle from the element’s properties.
    • Defining an appName property that defaults to "TodoMVC".
import { StacheElement } from "//unpkg.com/can@pre/core.mjs";

class TodoMVC extends StacheElement {
  static view = `
    <section id="todoapp">
      <header id="header">
        <h1>{{ this.appName }}</h1>
        <input id="new-todo" placeholder="What needs to be done?">
      </header>
      <section id="main">
        <input id="toggle-all" type="checkbox"/>
        <label for="toggle-all">Mark all as complete</label>
        <ul id="todo-list">
          <li class="todo">
            <div class="view">
              <input class="toggle" type="checkbox">
              <label>Do the dishes</label>
              <button class="destroy"></button>
            </div>
            <input class="edit" type="text" value="Do the dishes">
          </li>
          <li class="todo completed">
            <div class="view">
              <input class="toggle" type="checkbox">
              <label>Mow the lawn</label>
              <button class="destroy"></button>
            </div>
            <input class="edit" type="text" value="Mow the lawn">
          </li>
          <li class="todo editing">
            <div class="view">
              <input class="toggle" type="checkbox">
              <label>Pick up dry cleaning</label>
              <button class="destroy"></button>
            </div>
            <input class="edit" type="text" value="Pick up dry cleaning">
          </li>
        </ul>
      </section>
      <footer id="footer" class="">
        <span id="todo-count">
          <strong>2</strong> items left
        </span>
        <ul id="filters">
          <li>
            <a href="#!" class="selected">All</a>
          </li>
          <li>
            <a href="#!active">Active</a>
          </li>
          <li>
            <a href="#!complete">Completed</a>
          </li>
        </ul>
        <button id="clear-completed">
          Clear completed (1)
        </button>
      </footer>
    </section>
  `;

  static props = {
    appName: { default: "TodoMVC" }
  };
}
customElements.define("todo-mvc", TodoMVC);

When complete, you should see the same content as before. Only now, it’s rendered with a live-bound stache template. The live binding means that when the template’s data is changed, it will update automatically. You can see this by entering the following in the console:

document.querySelector("todo-mvc").appName = "My Todos";

Define the todos type and show the active and complete count

the problem

In this section, we will:

  • Create a list of todos and show them.
  • Show the number of active (complete === true) and complete todos.
  • Connect a todo’s complete property to a checkbox so that when we toggle the checkbox the number of active and complete todos changes.

the solution

In the JavaScript tab:

  • Define a Todo type with ObservableObject.
  • Define a Todo.List type along with an active and complete property with ObservableArray.

In <todo-mvc>’s props:

  • Create a list of todos and pass those to the template.

In <todo-mvc>’s view:

  • Use {{# for(of) }} to loop through every todo.
  • Add completed to the <li>’s className if the <li>’s todo is complete.
  • Use checked:bind to two-way bind the checkbox’s checked property to its todo’s complete property.
  • Use {{ todo.name }} to insert the value todo’s name as the content of the <label> and value of the text <input>.
  • Insert the active and complete number of todos.
import { ObservableArray, ObservableObject, StacheElement, type } from "//unpkg.com/can@pre/core.mjs";

class Todo extends ObservableObject {
  static props = {
    id: { type: type.convert(Number) },
    name: String,
    complete: { type: type.convert(Boolean), default: false }
  };
}

class TodoList extends ObservableArray {
  static items = Todo;

  static props = {
    get active() {
      return this.filter({ complete: false });
    },

    get complete() {
      return this.filter({ complete: true });
    }
  };
}

class TodoMVC extends StacheElement {
  static view = `
    <section id="todoapp">
      <header id="header">
        <h1>{{ this.appName }}</h1>
        <input id="new-todo" placeholder="What needs to be done?">
      </header>
      <section id="main" class="">
        <input id="toggle-all" type="checkbox">
        <label for="toggle-all">Mark all as complete</label>
        <ul id="todo-list">
          {{# for(todo of this.todos) }}
            <li class="todo {{# if(todo.complete) }}completed{{/ if }}">
              <div class="view">
                <input class="toggle" type="checkbox" checked:bind="todo.complete">
                <label>{{ todo.name }}</label>
                <button class="destroy"></button>
              </div>
              <input class="edit" type="text" value="{{ todo.name }}">
            </li>
          {{/ for }}
        </ul>
      </section>
      <footer id="footer" class="">
        <span id="todo-count">
          <strong>{{ this.todos.active.length }}</strong> items left
        </span>
        <ul id="filters">
          <li>
            <a href="#!" class="selected">All</a>
          </li>
          <li>
            <a href="#!active">Active</a>
          </li>
          <li>
            <a href="#!complete">Completed</a>
          </li>
        </ul>
        <button id="clear-completed">
          Clear completed ({{ this.todos.complete.length }})
        </button>
      </footer>
    </section>
  `;

  static props = {
    appName: { default: "TodoMVC" },
    todos: {
      get default() {
        return new TodoList([
          { id: 5, name: "mow lawn", complete: false },
          { id: 6, name: "dishes", complete: true },
          { id: 7, name: "learn canjs", complete: false }
        ]);
      }
    }
  };
}
customElements.define("todo-mvc", TodoMVC);

When complete, you should be able to toggle the checkboxes and see the number of items left and the completed count change automatically. This is because can-stache is able to listen for changes in observables like ObservableObject and ObservableArray.

Get todos from the server

the problem

In this section, we will:

  • Load todos from a RESTful service.
  • Fake that RESTful service.

the solution

In the Todo type:

  • Specify id as the identity property of the Todo type.

Update the JavaScript tab to:

  • Create a fake data store that is initialized with data for 3 todos with store.
  • Trap AJAX requests to "/api/todos" and provide responses with the data from the fake data store with can-fixture.
  • Connect the Todo and Todo.List types to the RESTful "/api/todos" endpoint using can-realtime-rest-model. This allows you to load, create, update, and destroy todos on the server.

In <todo-mvc>’s props:

  • Use getList to load a list of all todos on the server. The result of getList is a Promise that resolves to a Todo.List with the todos returned from the fake data store. That Promise is available to the template as this.todosPromise.

In <todo-mvc>’s view:

  • Use {{# for(todo of todosPromise.value) }} to loop through the promise’s resolved value, which is the list of todos returned by the server.
  • Read the active and completed number of todos from the promise’s resolved value.
import {
  fixture,
  ObservableArray,
  ObservableObject,
  realtimeRestModel,
  StacheElement,
  type
} from "//unpkg.com/can@pre/core.mjs";

class Todo extends ObservableObject {
  static props = {
    id: { type: type.convert(Number), identity: true },
    name: String,
    complete: { type: type.convert(Boolean), default: false }
  };
}

class TodoList extends ObservableArray {
  static items = Todo;

  static props = {
    get active() {
      return this.filter({ complete: false });
    },

    get complete() {
      return this.filter({ complete: true });
    }
  };
}

const todoStore = fixture.store(
  [
    { name: "mow lawn", complete: false, id: 5 },
    { name: "dishes", complete: true, id: 6 },
    { name: "learn canjs", complete: false, id: 7 }
  ],
  Todo
);

fixture("/api/todos", todoStore);
fixture.delay = 200;

realtimeRestModel({
  url: "/api/todos",
  Map: Todo,
  List: TodoList
});

class TodoMVC extends StacheElement {
  static view = `
    <section id="todoapp">
      <header id="header">
        <h1>{{ this.appName }}</h1>
        <input id="new-todo" placeholder="What needs to be done?">
      </header>
      <section id="main" class="">
        <input id="toggle-all" type="checkbox">
        <label for="toggle-all">Mark all as complete</label>
        <ul id="todo-list">
          {{# for(todo of this.todosPromise.value) }}
            <li class="todo {{# if(todo.complete) }}completed{{/ if }}">
              <div class="view">
                <input class="toggle" type="checkbox" checked:bind="todo.complete">
                <label>{{ todo.name }}</label>
                <button class="destroy"></button>
              </div>
              <input class="edit" type="text" value="{{ todo.name }}">
            </li>
          {{/ for }}
        </ul>
      </section>
      <footer id="footer" class="">
        <span id="todo-count">
          <strong>{{ this.todosPromise.value.active.length }}</strong> items left
        </span>
        <ul id="filters">
          <li>
            <a href="#!" class="selected">All</a>
          </li>
          <li>
            <a href="#!active">Active</a>
          </li>
          <li>
            <a href="#!complete">Completed</a>
          </li>
        </ul>
        <button id="clear-completed">
          Clear completed ({{ this.todosPromise.value.complete.length }})
        </button>
      </footer>
    </section>
  `;

  static props = {
    appName: { default: "TodoMVC" },
    get todosPromise() {
      return Todo.getList({});
    }
  };
}
customElements.define("todo-mvc", TodoMVC);

When complete, you’ll notice a 1 second delay before seeing the list of todos as they load from the fixtured data store.

Destroy todos

the problem

In this section, we will:

  • Delete a todo on the server when its destroy button is clicked.
  • Remove the todo from the page after it’s deleted.

the solution

Update <todo-mvc>’s view to:

  • Add destroying to the <li>’s className if the <li>’s todo is being destroyed using isDestroying.
  • Call the todo’s destroy method when the <button> is clicked using on:click.
import {
  fixture,
  ObservableArray,
  ObservableObject,
  realtimeRestModel,
  StacheElement,
  type
} from "//unpkg.com/can@pre/core.mjs";

class Todo extends ObservableObject {
  static props = {
    id: { type: type.convert(Number), identity: true },
    name: String,
    complete: { type: type.convert(Boolean), default: false }
  };
}

class TodoList extends ObservableArray {
  static items = Todo;

  static props = {
    get active() {
      return this.filter({ complete: false });
    },

    get complete() {
      return this.filter({ complete: true });
    }
  };
}

const todoStore = fixture.store(
  [
    { name: "mow lawn", complete: false, id: 5 },
    { name: "dishes", complete: true, id: 6 },
    { name: "learn canjs", complete: false, id: 7 }
  ],
  Todo
);

fixture("/api/todos", todoStore);
fixture.delay = 200;

realtimeRestModel({
  url: "/api/todos",
  Map: Todo,
  List: TodoList
});

class TodoMVC extends StacheElement {
  static view = `
    <section id="todoapp">
      <header id="header">
        <h1>{{ this.appName }}</h1>
        <input id="new-todo" placeholder="What needs to be done?">
      </header>
      <section id="main" class="">
        <input id="toggle-all" type="checkbox">
        <label for="toggle-all">Mark all as complete</label>
        <ul id="todo-list">
          {{# for(todo of this.todosPromise.value) }}
            <li class="todo {{# if(todo.complete) }}completed{{/ if }}
              {{# if(todo.isDestroying()) }}destroying{{/ if }}">
              <div class="view">
                <input class="toggle" type="checkbox" checked:bind="todo.complete">
                <label>{{ todo.name }}</label>
                <button class="destroy" on:click="todo.destroy()"></button>
              </div>
              <input class="edit" type="text" value="{{todo.name}}">
            </li>
          {{/ for }}
        </ul>
      </section>
      <footer id="footer" class="">
        <span id="todo-count">
          <strong>{{ this.todosPromise.value.active.length }}</strong> items left
        </span>
        <ul id="filters">
          <li>
            <a href="#!" class="selected">All</a>
          </li>
          <li>
            <a href="#!active">Active</a>
          </li>
          <li>
            <a href="#!complete">Completed</a>
          </li>
        </ul>
        <button id="clear-completed">
          Clear completed ({{ this.todosPromise.value.complete.length }})
        </button>
      </footer>
    </section>
  `;

  static props = {
    appName: { default: "TodoMVC" },
    get todosPromise() {
      return Todo.getList({});
    }
  };
}
customElements.define("todo-mvc", TodoMVC);

When complete, you should be able to delete a todo by clicking its delete button. After clicking the todo, its name will turn red and italic. Once deleted, the todo will be automatically removed from the page.

The deleted todo is automatically removed from the page because can-realtime-rest-model adds the real-time behavior. The real-time behavior automatically updates lists (like Todo.List) when instances are created, updated or destroyed.

Create todos

the problem

In this section, we will:

  • Define a custom <todo-create> element that can create todos on the server.
  • Use that custom element.

the solution

The <todo-create> component will respond to a user hitting the enter key. The can-event-dom-enter event provides this functionality, but it is an Ecosystem module. So to use the enter event we need to:

  • Import enterEvent from everything.mjs (which includes enterEvent) instead of core.mjs.
  • Import domEvents (CanJS’s global event registry).
  • Add the enterEvent to domEvents.

Update the JavaScript tab to define a <todo-create> component with the following:

  • A view that:
    • Updates the todo’s name with the <input>’s value using value:bind.
    • Calls createTodo when the enter key is pressed using on:enter.
  • The following [can-stache-element.html#Defininganelement_sproperties props]:
    • A todo property that holds a new Todo instance.
    • A createTodo method that saves the Todo instance and replaces it with a new one once saved.

Update <todo-mvc>’s view to:

  • Use the <todo-create> component.

This results in the following code:

import {
  StacheElement,
  ObservableArray,
  ObservableObject,
  fixture,
  realtimeRestModel,
  type,
  domEvents,
  enterEvent
} from "//unpkg.com/can@pre/everything.mjs";

domEvents.addEvent(enterEvent);

class Todo extends ObservableObject {
  static props = {
    id: { type: type.convert(Number), identity: true },
    name: String,
    complete: { type: type.convert(Boolean), default: false }
  };
}

class TodoListModel extends ObservableArray {
  static items = Todo;

  static props = {
    get active() {
      return this.filter({ complete: false });
    },

    get complete() {
      return this.filter({ complete: true });
    }
  };
}

const todoStore = fixture.store(
  [
    { name: "mow lawn", complete: false, id: 5 },
    { name: "dishes", complete: true, id: 6 },
    { name: "learn canjs", complete: false, id: 7 }
  ],
  Todo
);

fixture("/api/todos", todoStore);
fixture.delay = 200;

realtimeRestModel({
  url: "/api/todos",
  Map: Todo,
  List: TodoListModel
});

class TodoCreate extends StacheElement {
  static view = `
    <input id="new-todo"
      placeholder="What needs to be done?"
      value:bind="this.todo.name"
      on:enter="this.createTodo()"
    >
  `;

  static props = {
    todo: {
      get default() {
        return new Todo();
      }
    }
  };

  createTodo() {
    this.todo.save().then(() => {
      this.todo = new Todo();
    });
  }
}
customElements.define("todo-create", TodoCreate);

class TodoMVC extends StacheElement {
  static view = `
    <section id="todoapp">
      <header id="header">
        <h1>{{ this.appName }}</h1>
        <todo-create />
      </header>
      <section id="main" class="">
        <input id="toggle-all" type="checkbox">
        <label for="toggle-all">Mark all as complete</label>
        <ul id="todo-list">
          {{# for(todo of this.todosPromise.value) }}
            <li class="todo {{# if(todo.complete) }}completed{{/ if }}
              {{# if(todo.isDestroying()) }}destroying{{/ if }}">
              <div class="view">
                <input class="toggle" type="checkbox" checked:bind="todo.complete">
                <label>{{ todo.name }}</label>
                <button class="destroy" on:click="todo.destroy()"></button>
              </div>
              <input class="edit" type="text" value="{{ todo.name }}">
            </li>
          {{/ for }}
        </ul>
      </section>
      <footer id="footer" class="">
        <span id="todo-count">
          <strong>{{ this.todosPromise.value.active.length }}</strong> items left
        </span>
        <ul id="filters">
          <li>
            <a href="#!" class="selected">All</a>
          </li>
          <li>
            <a href="#!active">Active</a>
          </li>
          <li>
            <a href="#!complete">Completed</a>
          </li>
        </ul>
        <button id="clear-completed">
          Clear completed ({{ this.todosPromise.value.complete.length }})
        </button>
      </footer>
    </section>
  `;

  static props = {
    appName: { default: "TodoMVC" },
    get todosPromise() {
      return Todo.getList({});
    }
  };
}
customElements.define("todo-mvc", TodoMVC);

When complete, you will be able to create a todo by typing the name of the todo and pressing enter. Notice that the new todo automatically appears in the list of todos. This is also because can-realtime-rest-model automatically inserts newly created items into lists that they belong within.

List todos

the problem

In this section, we will:

  • Define a custom element for showing a list of todos.
  • Use that custom element by passing it the list of fetched todos.

the solution

Update the JavaScript tab to:

  • Define a <todo-list> component with:
    • A todos property that should be provided by the parent element.
    • A view that loops through a list of todos (instead of todosPromise.value).
  • In <todo-mvc>’s view, create a <todo-list> element and set its todos property to the resolved value of todosPromise using todos:from='this.todosPromise.value'.
import {
  StacheElement,
  ObservableArray,
  ObservableObject,
  fixture,
    realtimeRestModel,
    type,
  domEvents,
  enterEvent
} from "//unpkg.com/can@pre/everything.mjs";

domEvents.addEvent(enterEvent);

class Todo extends ObservableObject {
  static props = {
    id: { type: type.convert(Number), identity: true },
    name: String,
    complete: { type: type.convert(Boolean), default: false }
  };
}

class TodoList extends ObservableArray {
  static items = Todo;

  static props = {
    get active() {
      return this.filter({ complete: false });
    },

    get complete() {
      return this.filter({ complete: true });
    }
  };
}

const todoStore = fixture.store(
  [
    { name: "mow lawn", complete: false, id: 5 },
    { name: "dishes", complete: true, id: 6 },
    { name: "learn canjs", complete: false, id: 7 }
  ],
  Todo
);

fixture("/api/todos", todoStore);
fixture.delay = 200;

realtimeRestModel({
  url: "/api/todos",
  Map: Todo,
  List: TodoList
});

class TodoCreate extends StacheElement {
  static view = `
    <input id="new-todo"
      placeholder="What needs to be done?"
      value:bind="this.todo.name"
      on:enter="this.createTodo()"
    >
  `;

  static props = {
    todo: {
      get default() {
        return new Todo();
      }
    }
  };

  createTodo() {
    this.todo.save().then(() => {
      this.todo = new Todo();
    });
  }
}
customElements.define("todo-create", TodoCreate);

class TodoListElement extends StacheElement {
  static view = `
    <ul id="todo-list">
      {{# for(todo of this.todos) }}
        <li class="todo {{# if(todo.complete) }}completed{{/ if }}
          {{# if(todo.isDestroying()) }}destroying{{/ if }}">
          <div class="view">
            <input class="toggle" type="checkbox" checked:bind="todo.complete">
            <label>{{ todo.name }}</label>
            <button class="destroy" on:click="todo.destroy()"></button>
          </div>
          <input class="edit" type="text" value="{{ todo.name }}">
        </li>
      {{/ for }}
    </ul>
  `;

  static props = {
    todos: type.maybeConvert(TodoList)
  };
}
customElements.define("todo-list", TodoListElement);

class TodoMVC extends StacheElement {
  static view = `
    <section id="todoapp">
      <header id="header">
        <h1>{{ this.appName }}</h1>
        <todo-create />
      </header>
      <section id="main" class="">
        <input id="toggle-all" type="checkbox">
        <label for="toggle-all">Mark all as complete</label>
        <todo-list todos:from="this.todosPromise.value" />
      </section>
      <footer id="footer" class="">
        <span id="todo-count">
          <strong>{{ this.todosPromise.value.active.length }}</strong> items left
        </span>
        <ul id="filters">
          <li>
            <a href="#!" class="selected">All</a>
          </li>
          <li>
            <a href="#!active">Active</a>
          </li>
          <li>
            <a href="#!complete">Completed</a>
          </li>
        </ul>
        <button id="clear-completed">
          Clear completed ({{ this.todosPromise.value.complete.length }})
        </button>
      </footer>
    </section>
  `;

  static props = {
    appName: { default: "TodoMVC" },
    get todosPromise() {
      return Todo.getList({});
    }
  };
}
customElements.define("todo-mvc", TodoMVC);

When complete, everything should work the same. We didn’t add any new functionality, we just moved code around to make it more isolated, potentially reusable, and more maintainable.

Edit todos

the problem

In this section, we will:

  • Make it possible to edit a todo’s name and save that change to the server.

the solution

Update the <todo-list>’s view to:

  • Use the isEditing method to add editing to the className of the <li> being edited.
  • When the checkbox changes, update the todo on the server with save,
  • Call edit with the current todo.
  • Set up the edit input to:
    • Two-way bind its value to the current todo’s name using value:bind.
    • Call updateName when the enter key is pressed using on:enter.
    • Focus the input when isEditing is true using the special [can-util/dom/attr/attr.special.focused] attribute.
    • Call cancelEdit if the input element loses focus.

Update the <todo-list>’s props to include the methods and properties needed to edit a todo’s name, including:

  • An editing property of type Todo that stores which todo is being edited.
  • A backupName property that stores the todo’s name before being edited.
  • An edit method that sets up the editing state.
  • A cancelEdit method that undos the editing state if in the editing state.
  • An updateName method that updates the editing todo and saves it to the server.
import {
  StacheElement,
  ObservableArray,
  ObservableObject,
  fixture,
  realtimeRestModel,
  type,
  domEvents,
  enterEvent
} from "//unpkg.com/can@pre/everything.mjs";

domEvents.addEvent(enterEvent);

class Todo extends ObservableObject {
  static props = {
    id: { type: type.convert(Number), identity: true },
    name: String,
    complete: { type: type.convert(Boolean), default: false }
  };
}

class TodoList extends ObservableArray {
  static items = Todo;

  static props = {
    get active() {
      return this.filter({ complete: false });
    },

    get complete() {
      return this.filter({ complete: true });
    }
  };
}

const todoStore = fixture.store(
  [
    { name: "mow lawn", complete: false, id: 5 },
    { name: "dishes", complete: true, id: 6 },
    { name: "learn canjs", complete: false, id: 7 }
  ],
  Todo
);

fixture("/api/todos", todoStore);
fixture.delay = 200;

realtimeRestModel({
  url: "/api/todos",
  Map: Todo,
  List: TodoList
});

class TodoCreate extends StacheElement {
  static view = `
    <input id="new-todo"
      placeholder="What needs to be done?"
      value:bind="this.todo.name"
      on:enter="this.createTodo()"
    >
  `;

  static props = {
    todo: {
      get default() {
        return new Todo();
      }
    }
  };

  createTodo() {
    this.todo.save().then(() => {
      this.todo = new Todo();
    });
  }
}
customElements.define("todo-create", TodoCreate);

class TodoListElement extends StacheElement {
  static view = `
    <ul id="todo-list">
      {{# for(todo of this.todos) }}
        <li class="todo {{# if(todo.complete) }}completed{{/ if }}
          {{# if(todo.isDestroying()) }}destroying{{/ if }}
          {{# if(this.isEditing(todo)) }}editing{{/ if }}">
          <div class="view">
            <input 
              class="toggle"
              type="checkbox"
              checked:bind="todo.complete"
              on:change="todo.save()"
            >
            <label on:dblclick="this.edit(todo)">{{ todo.name }}</label>
            <button class="destroy" on:click="todo.destroy()"></button>
          </div>
          <input 
            class="edit"
            type="text"
            value:bind="todo.name"
            on:enter="this.updateName()"
            focused:from="this.isEditing(todo)"
            on:blur="this.cancelEdit()"
          >
        </li>
      {{/ for }}
    </ul>
  `;

  static props = {
    todos: type.maybeConvert(TodoListModel),
    editing: Todo,
    backupName: String
  };

  isEditing(todo) {
    return todo === this.editing;
  }

  edit(todo) {
    this.backupName = todo.name;
    this.editing = todo;
  }

  cancelEdit() {
    if (this.editing) {
      this.editing.name = this.backupName;
    }
    this.editing = null;
  }

  updateName() {
    this.editing.save();
    this.editing = null;
  }
}
customElements.define("todo-list", TodoListElement);

class TodoMVC extends StacheElement {
  static view = `
    <section id="todoapp">
      <header id="header">
        <h1>{{ this.appName }}</h1>
        <todo-create />
      </header>
      <section id="main" class="">
        <input id="toggle-all" type="checkbox">
        <label for="toggle-all">Mark all as complete</label>
        <todo-list todos:from="this.todosPromise.value" />
      </section>
      <footer id="footer" class="">
        <span id="todo-count">
          <strong>{{ this.todosPromise.value.active.length }}</strong> items left
        </span>
        <ul id="filters">
          <li>
            <a href="#!" class="selected">All</a>
          </li>
          <li>
            <a href="#!active">Active</a>
          </li>
          <li>
            <a href="#!complete">Completed</a>
          </li>
        </ul>
        <button id="clear-completed">
          Clear completed ({{ this.todosPromise.value.complete.length }})
        </button>
      </footer>
    </section>
  `;

  static props = {
    appName: { default: "TodoMVC" },
    get todosPromise() {
      return Todo.getList({});
    }
  };
}
customElements.define("todo-mvc", TodoMVC);

When complete, you should be able to edit a todo’s name.

Routing

the problem

In this section, we will:

  • Make it possible to use the browser’s forwards and backwards buttons to change between showing all todos, only active todos, or only completed todos.
  • Add links to change between showing all todos, only active todos, or only completed todos.
  • Make those links bold when the site is currently showing that link.

the solution

Update the JavaScript tab to:

  • Import route.

Update <todo-mvc>’s view to:

  • Set the page links hrefs to a URL that will set the desired properties on route.data when clicked using routeUrl(hashes).
  • Add class='selected' to the link if the current route matches the current properties on route.data using routeCurrent(hash).

Update <todo-mvc>’s props to:

  • Provide access to the observable route.data by:
    • Defining a routeData property whose value is route.data.
    • Create a pretty routing rule so if the URL looks like "#!active", the filter property of route.data will be set to "active" with route.register.
    • Initialize route.data’s values with route.start().
  • Change todosPromise to check if this.routeData.filter is:
    • falsy - then return all todos.
    • "complete" - then return all complete todos.
    • "incomplete" - then return all incomplete todos.
import {
  StacheElement,
  ObservableArray,
  ObservableObject,
  fixture,
  realtimeRestModel,
  type,
  domEvents,
  enterEvent,
  route
} from "//unpkg.com/can@pre/everything.mjs";

domEvents.addEvent(enterEvent);

class Todo extends ObservableObject {
  static props = {
    id: { type: type.convert(Number), identity: true },
    name: String,
    complete: { type: type.convert(Boolean), default: false }
  };
}

class TodoList extends ObservableArray {
  static items = Todo;

  static props = {
    get active() {
      return this.filter({ complete: false });
    },

    get complete() {
      return this.filter({ complete: true });
    }
  };
}

const todoStore = fixture.store(
  [
    { name: "mow lawn", complete: false, id: 5 },
    { name: "dishes", complete: true, id: 6 },
    { name: "learn canjs", complete: false, id: 7 }
  ],
  Todo
);

fixture("/api/todos", todoStore);
fixture.delay = 200;

realtimeRestModel({
  url: "/api/todos",
  Map: Todo,
  List: TodoList
});

class TodoCreate extends StacheElement {
  static view = `
    <input id="new-todo"
      placeholder="What needs to be done?"
      value:bind="this.todo.name"
      on:enter="this.createTodo()"
    >
  `;

  static props = {
    todo: {
      get default() {
        return new Todo();
      }
    }
  };

  createTodo() {
    this.todo.save().then(() => {
      this.todo = new Todo();
    });
  }
}
customElements.define("todo-create", TodoCreate);

class TodoListElement extends StacheElement {
  static view = `
    <ul id="todo-list">
      {{# for(todo of this.todos) }}
        <li class="todo {{# if(todo.complete) }}completed{{/ if }}
          {{# if(todo.isDestroying()) }}destroying{{/ if }}
          {{# if(this.isEditing(todo)) }}editing{{/ if }}">
          <div class="view">
            <input 
              class="toggle"
              type="checkbox"
              checked:bind="todo.complete"
              on:change="todo.save()"
            >
            <label on:dblclick="this.edit(todo)">{{ todo.name }}</label>
            <button class="destroy" on:click="todo.destroy()"></button>
          </div>
          <input 
            class="edit"
            type="text"
            value:bind="todo.name"
            on:enter="this.updateName()"
            focused:from="this.isEditing(todo)"
            on:blur="this.cancelEdit()"
          >
        </li>
      {{/ for }}
    </ul>
  `;

  static props = {
    todos: type.maybeConvert(TodoListModel),
    editing: Todo,
    backupName: String
  };

  isEditing(todo) {
    return todo === this.editing;
  }

  edit(todo) {
    this.backupName = todo.name;
    this.editing = todo;
  }

  cancelEdit() {
    if (this.editing) {
      this.editing.name = this.backupName;
    }
    this.editing = null;
  }

  updateName() {
    this.editing.save();
    this.editing = null;
  }
}
customElements.define("todo-list", TodoListElement);

class TodoMVC extends StacheElement {
  static view = `
    <section id="todoapp">
      <header id="header">
        <h1>{{ this.appName }}</h1>
        <todo-create />
      </header>
      <section id="main" class="">
        <input id="toggle-all" type="checkbox">
        <label for="toggle-all">Mark all as complete</label>
        <todo-list todos:from="this.todosPromise.value" />
      </section>
      <footer id="footer" class="">
        <span id="todo-count">
          <strong>{{ this.todosPromise.value.active.length }}</strong> items left
        </span>
        <ul id="filters">
          <li>
            <a href="{{ routeUrl(filter=undefined) }}"
              {{# routeCurrent(filter=undefined) }}class="selected"{{/ routeCurrent }}>All</a>
          </li>
          <li>
            <a href="{{ routeUrl(filter='active') }}"
              {{# routeCurrent(filter='active') }}class="selected"{{/ routeCurrent }}>Active</a>
          </li>
          <li>
            <a href="{{ routeUrl(filter='complete') }}"
              {{# routeCurrent(filter='complete') }}class="selected"{{/ routeCurrent }}>Completed</a>
          </li>
        </ul>
        <button id="clear-completed">
          Clear completed ({{ this.todosPromise.value.complete.length }})
        </button>
      </footer>
    </section>
  `;

  static props = {
    appName: { default: "TodoMVC" },
    routeData: {
      get default() {
        route.register("{filter}");
        route.start();
        return route.data;
      }
    },
    get todosPromise() {
      if (!this.routeData.filter) {
        return Todo.getList({});
      } else {
        return Todo.getList({
          filter: { complete: this.routeData.filter === "complete" }
        });
      }
    }
  };
}
customElements.define("todo-mvc", TodoMVC);

When complete, you should be able to click the All, Active, and Completed links and see the right data.

Toggle all and clear completed

the problem

In this section, we will:

  • Make the toggle-all button change all todos to complete or incomplete.
  • Make the clear-completed button delete all complete todos.

the solution

Add the following to the Todo.List model:

  • An allComplete property that returns true if every todo is complete.
  • A saving property that returns todos that are being saved using isSaving.
  • An updateCompleteTo method that updates every todo’s complete property to the specified value and updates the compute on the server with save.
  • A destroyComplete method that deletes every complete todo with destroy.

Update <todo-mvc>’s view to:

  • Cross bind the toggle-all <input>’s checked property to the <todo-mvc>’s allChecked property.
  • Disable the toggle-all button while any todo is saving.
  • Call the Todo.List’s destroyComplete method when the clear-completed button is clicked on.

Update <todo-mvc>’s props to include:

  • A todosList property that gets its value from the todosPromise using an asynchronous getter.
  • An allChecked property that returns true if every todo is complete. The property can also be set to true or false and it will set every todo to that value.
import {
  domEvents,
  enterEvent,
  fixture,
  ObservableArray,
  ObservableObject,
  realtimeRestModel,
  route,
  StacheElement,
  type
} from "//unpkg.com/can@pre/everything.mjs";

domEvents.addEvent(enterEvent);

class Todo extends ObservableObject {
  static props = {
    id: { type: type.convert(Number), identity: true },
    name: String,
    complete: false
  };
}

class TodoList extends ObservableArray {
    static items = Todo;

  static props = {
    get active() {
      return this.filter({ complete: false });
    },

    get complete() {
      return this.filter({ complete: true });
    },

    get allComplete() {
      return this.length === this.complete.length;
    },

    get saving() {
      return this.filter(function(todo) {
        return todo.isSaving();
      });
    }
  };

  updateCompleteTo(value) {
    this.forEach(function(todo) {
      todo.complete = value;
      todo.save();
    });
  }

  destroyComplete() {
    this.complete.forEach(function(todo) {
      todo.destroy();
    });
  }
}

const todoStore = fixture.store(
  [
    { name: "mow lawn", complete: false, id: 5 },
    { name: "dishes", complete: true, id: 6 },
    { name: "learn canjs", complete: false, id: 7 }
  ],
  Todo
);

fixture("/api/todos", todoStore);
fixture.delay = 200;

realtimeRestModel({
  url: "/api/todos",
  Map: Todo,
  List: TodoList
});

class TodoCreate extends StacheElement {
  static view = `
    <input id="new-todo"
      placeholder="What needs to be done?"
      value:bind="this.todo.name"
      on:enter="this.createTodo()"
    >
  `;

  static props = {
    todo: {
      get default() {
        return new Todo();
      }
    }
  };

  createTodo() {
    this.todo.save().then(() => {
      this.todo = new Todo();
    });
  }
}

customElements.define("todo-create", TodoCreate);

class TodoListElement extends StacheElement {
  static view = `
    <ul id="todo-list">
      {{# for(todo of this.todos) }}
        <li class="todo {{# if(todo.complete) }}completed{{/ if }}
          {{# if(todo.isDestroying()) }}destroying{{/ if }}
          {{# if(this.isEditing(todo)) }}editing{{/ if }}">
          <div class="view">
            <input 
              class="toggle"
              type="checkbox"
              checked:bind="todo.complete"
              on:change="todo.save()"
            >
            <label on:dblclick="this.edit(todo)">{{ todo.name }}</label>
            <button class="destroy" on:click="todo.destroy()"></button>
          </div>
          <input 
            class="edit"
            type="text"
            value:bind="todo.name"
            on:enter="this.updateName()"
            focused:from="this.isEditing(todo)"
            on:blur="this.cancelEdit()"
          >
        </li>
      {{/ for }}
    </ul>
  `;

  static props = {
    todos: type.maybeConvert(TodoList),
    editing: Todo,
    backupName: String
  };

  isEditing(todo) {
    return todo === this.editing;
  }

  edit(todo) {
    this.backupName = todo.name;
    this.editing = todo;
  }

  cancelEdit() {
    if (this.editing) {
      this.editing.name = this.backupName;
    }
    this.editing = null;
  }

  updateName() {
    this.editing.save();
    this.editing = null;
  }
}

customElements.define("todo-list", TodoListElement);

class TodoMVC extends StacheElement {
  static view = `
    <section id="todoapp">
      <header id="header">
        <h1>{{ this.appName }}</h1>
        <todo-create/>
      </header>
      <section id="main">
        <input 
          id="toggle-all" 
          type="checkbox"
          checked:bind="this.allChecked"
          disabled:from="this.todosList.saving.length"
        >
        <label for="toggle-all">Mark all as complete</label>
        <todo-list todos:from="this.todosPromise.value"/>
      </section>
      <footer id="footer">
        <span id="todo-count">
          <strong>{{ this.todosPromise.value.active.length }}</strong> items left
        </span>
        <ul id="filters">
          <li>
            <a href="{{ routeUrl(filter=undefined) }}"
              {{# routeCurrent(filter=undefined) }}class="selected"{{/ routeCurrent }}>All</a>
          </li>
          <li>
            <a href="{{ routeUrl(filter='active') }}"
              {{# routeCurrent(filter='active') }}class="selected"{{/ routeCurrent }}>Active</a>
          </li>
          <li>
            <a href="{{ routeUrl(filter='complete') }}"
              {{# routeCurrent(filter='complete') }}class="selected"{{/ routeCurrent }}>Completed</a>
          </li>
        </ul>
        <button id="clear-completed" on:click="this.todosList.destroyComplete()">
          Clear completed ({{ this.todosPromise.value.complete.length }})
        </button>
      </footer>
    </section>
  `;

  static props = {
    appName: { default: "TodoMVC" },
    routeData: {
      get default() {
        route.register("{filter}");
        route.start();
        return route.data;
      }
    },
    get todosPromise() {
      if (!this.routeData.filter) {
        return Todo.getList({});
      } else {
        return Todo.getList({
          filter: { complete: this.routeData.filter === "complete" }
        });
      }
    },
    todosList: {
      async(resolve) {
        this.todosPromise.then(resolve);
      }
    },
    get allChecked() {
      return this.todosList && this.todosList.allComplete;
    },
    set allChecked(newVal) {
      this.todosList && this.todosList.updateCompleteTo(newVal);
    }
  };
}
customElements.define("todo-mvc", TodoMVC);

When complete, you should be able to toggle all todos complete state and delete the completed todos. You should also have a really good idea how CanJS works!

Result

When finished, you should see something like the following CodePen:

See the Pen CanJS 6.0 - TodoMVC End by Bitovi (@bitovi) on CodePen.

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