DoneJS StealJS jQuery++ FuncUnit DocumentJS
3.14.1
5.0.0 4.3.0 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • experiment
      • Chat Guide
      • TodoMVC Guide
      • ATM Guide
    • getting started
      • Setting Up CanJS
      • Reading the Docs (API Guide)
    • recipes
      • Credit Card Guide (Advanced)
      • Credit Card Guide (Simple)
      • CTA Bus Map (Medium)
      • File Navigator Guide (Advanced)
      • File Navigator Guide (Simple)
      • Playlist Editor (Advanced)
      • Signup and Login (Simple)
      • TodoMVC with StealJS
    • upgrade
      • Migrating to CanJS 3
      • Using Codemods
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

TodoMVC with StealJS

  • Edit on GitHub

This guide walks through building TodoMVC with StealJS.

Setup (Framework Overview)

The problem

  • Setup steal to load a basic CanJS application. A basic CanJS application has:
    • A can-define/map/map ViewModel and an instance of that ViewModel.
    • A can-stache view that is rendered with the instance of the ViewModel.
  • In addition, this application should load the can-todomvc-test module version 1.0 and pass it the application’s ViewModel instance. You will need to declare the version explicitly as different versions of this guide depend on different versions of this package.

What you need to know

  • To create a new project with StealJS, run:

    npm init
    npm install steal steal-tools steal-css --save-dev
    
  • To host static files, install http-server and run it like:

    npm install http-server -g
    http-server -c-1
    
  • If you load StealJS plugins, add them to your package.json configuration like:

    "steal": {
      "plugins": [
        "steal-css"
      ]
    }
    
  • Define a ViewModel type with can-define/map/map:

    var DefineMap = require("can-define/map/");
    var Type = DefineMap.extend({ ... });
    
  • Create an instance of a ViewModel by using new Type(props):

    var instance = new Type({ ... });
    
  • Load a view with the steal-stache plugin like:

    var view = require("./path/to/template.stache");
    

    Note that steal-stache is a StealJS plugin and needs to be configured as such.

  • Render a view (or template) by passing it data. It returns a document fragment that can
    be inserted into the page like:

    var frag = view(appVM);
    document.body.appendChild(frag);
    
  • Use the following HTML that a designer might have provided:

    <section id="todoapp">
        <header id="header">
            <h1>Todos</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">
                <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 class="selected" href="#!">All</a>
                </li>
                <li>
                    <a href="#!active">Active</a>
                </li>
                <li>
                    <a href="#!completed">Completed</a>
                </li>
            </ul>
            <button id="clear-completed">
                Clear completed (1)
            </button>
        </footer>
    </section>
    
  • Use can-todomvc-test to load the application’s styles and run its tests:

    require("can-todomvc-test")(appVM);
    

The solution

Create a folder:

mkdir todomvc
cd todomvc

Host it:

npm install http-server -g
http-server -c-1

Create a new project:

npm init -y

Install steal, steal-tools, and CanJS’s core modules:

npm install steal steal-tools steal-css --save-dev
npm install can-define can-stache steal-stache --save

Add steal.plugins to package.json:

{
  "name": "todomvc",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "steal": "^1.3.0",
    "steal-css": "^1.2.1",
    "steal-tools": "^1.2.0"
  },
  "dependencies": {
    "can-define": "^1.0.16",
    "can-stache": "^3.0.20",
    "steal-stache": "^3.0.5"
  },
  "steal": {
    "plugins": [
      "steal-stache", "steal-css"
    ]
  }
}

Create the starting HTML page:

<!-- index.html -->
<script src="./node_modules/steal/steal.js"></script>

Create the application template:

<!-- index.stache -->
<section id="todoapp">
    <header id="header">
        <h1>{{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">
            <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 class="selected" href="#!">All</a>
            </li>
            <li>
                <a href="#!active">Active</a>
            </li>
            <li>
                <a href="#!completed">Completed</a>
            </li>
        </ul>
        <button id="clear-completed">
            Clear completed (1)
        </button>
    </footer>
</section>

Install the test harness:

npm install can-todomvc-test@1.0 --save-dev

Create the main app

// index.js
var view = require("./index.stache");
var DefineMap = require("can-define/map/");

var AppViewModel = DefineMap.extend("AppViewModel",{
    appName: "string"
});

var appVM = window.appVM = new AppViewModel({
    appName: "TodoMVC"
});

var frag = view(appVM);
document.body.appendChild(frag);

require("can-todomvc-test")(appVM);

Define Todo type (DefineMap basics)

The problem

  • Define a Todo type as the export of models/todo.js, where:
    • It is a can-define/map/map type.
    • The id or name property values are coerced into a string.
    • Its complete property is a Boolean that defaults to false.
    • It has a toggleComplete method that flips complete to the opposite value.

Example test code:

var todo = new Todo({id: 1, name: 2});
QUnit.equal(todo.id, "1", "id is a string");
QUnit.equal(todo.name, "2", "name is a string");
QUnit.equal(todo.complete, false, "complete defaults to false");
todo.toggleComplete();
QUnit.equal(todo.complete, true, "toggleComplete works");

What you need to know

  • DefineMap Basics Presentation

  • DefineMap.extend defines a new Type.

  • The type behavior defines a property’s type like:

    DefineMap.extend({
        propertyName: {type: "number"}
    })
    
  • The value behavior defines a property’s initial value like:

    DefineMap.extend({
        propertyName: {value: 3}
    })
    
  • Methods can be defined directly on the prototype like:

    DefineMap.extend({
        methodName: function(){}
    })
    

The solution

Create models/todo.js as follows:

// models/todo.js
var DefineMap = require("can-define/map/");

var Todo = DefineMap.extend("Todo", {
    id: "string",
    name: "string",
    complete: {
        type: "boolean",
        value: false
    },
    toggleComplete: function() {
        this.complete = !this.complete;
    }
});

module.exports = Todo;

Define Todo.List type (DefineList basics)

The problem

  • Define a Todo.List type on the export of models/todo.js, where:
    • It is a can-define/list/list type.
    • The enumerable indexes are coerced into Todo types.
    • Its .active property returns a filtered Todo.List of the todos that are not complete.
    • Its .complete property returns a filtered Todo.List of the todos that are complete.
    • Its .allComplete property true if all the todos are complete.

Example test code:

QUnit.ok(Todo.List, "Defined a List");
var todos = new Todo.List([{complete: true},{},{complete: true}]);
QUnit.ok(todos[0] instanceof Todo, "each item in a Todo.List is a Todo");
QUnit.equal(todos.active.length, 1);
QUnit.equal(todos.complete.length, 2);
QUnit.equal(todos.allComplete, false, "not allComplete");
todos[1].complete = true;
QUnit.equal(todos.allComplete, true, "allComplete");

What you need to know

  • DefineList Basics Presentation

  • DefineList.extend defines a new ListType.

  • The # property defines the behavior of items in a list like:

    DefineList.extend({
        #: {type: ItemType}
    })
    
  • The get behavior defines observable computed properties like:

    DefineMap.extend({
        propertyName: {
            get: function(){
                return this.otherProperty;
            }
        }
    })
    
  • filter can be used to filter a list into a new list:

    list = new ListType([...]);
    list.filter(function(item){
        return test(item);
    })
    

The solution

Update models/todo.js to the following:

// models/todo.js
var DefineMap = require("can-define/map/");
var DefineList = require("can-define/list/");

var Todo = DefineMap.extend("Todo", {
    id: "string",
    name: "string",
    complete: {
        type: "boolean",
        value: false
    },
    toggleComplete: function() {
        this.complete = !this.complete;
    }
});

Todo.List = DefineList.extend("TodoList", {
    "#": Todo,
    get active() {
        return this.filter({
            complete: false
        });
    },
    get complete() {
        return this.filter({
            complete: true
        });
    },
    get allComplete() {
        return this.length === this.complete.length;
    }
});

module.exports = Todo;

Render a list of todos (can-stache)

The problem

  • Add a todosList property to the AppViewModel whose default value will be a Todo.List with the following data:

    [
      { name: "mow lawn", complete: false, id: 5 },
      { name: "dishes", complete: true, id: 6 },
      { name: "learn canjs", complete: false, id: 7 }
    ]
    
  • Write out an <li> for each todo in todosList, including:

    • write the todo’s name in the <label>
    • add completed in the <li>’s class if the todo is complete.
    • check the todo’s checkbox if the todo is complete.
  • Write out the number of items left and completed count in the “Clear completed” button.

What you need to know

  • Stache Basics Presentation

  • CanJS uses can-stache to render data in a template and keep it live. Templates can be loaded with steal-stache.

    A can-stache template uses {{key}} magic tags to insert data into the HTML output like:

      {{something.name}}
    
  • Use {{#if(value)}} to do if/else branching in can-stache.

  • Use {{#each(value)}} to do looping in can-stache.

The solution

Update index.js to the following:

// index.js
var view = require("./index.stache");
var DefineMap = require("can-define/map/");
var Todo = require("~/models/todo");

var AppViewModel = DefineMap.extend("AppViewModel",{
    appName: "string",
    todosList: {
        value: function(){
            return new Todo.List([
                { name: "mow lawn", complete: false, id: 5 },
                { name: "dishes", complete: true, id: 6 },
                { name: "learn canjs", complete: false, id: 7 }
            ]);
        }
    }
});

var appVM = window.appVM = new AppViewModel({
    appName: "TodoMVC"
});

var frag = view(appVM);
document.body.appendChild(frag);

require("can-todomvc-test")(appVM);

Update index.stache to the following:

<!-- index.stache -->
<section id="todoapp">
    <header id="header">
        <h1>{{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">
            {{#each(todosList)}}
            <li class="todo {{#if(complete)}}completed{{/if}}">
                <div class="view">
                    <input class="toggle" type="checkbox"
                        {{#if(complete)}}checked{{/if}}/>
                    <label>{{name}}</label>
                    <button class="destroy"></button>
                </div>
                <input class="edit" type="text" value="{{name}}"/>
            </li>
            {{/each}}
        </ul>
    </section>
    <footer id="footer" class="">
        <span id="todo-count">
            <strong>{{todosList.active.length}}</strong> items left
        </span>
        <ul id="filters">
            <li>
                <a class="selected" href="#!">All</a>
            </li>
            <li>
                <a href="#!active">Active</a>
            </li>
            <li>
                <a href="#!completed">Completed</a>
            </li>
        </ul>
        <button id="clear-completed">
            Clear completed ({{todosList.complete.length}})
        </button>
    </footer>
</section>

Toggle a todo’s completed state (event bindings)

The problem

  • Call toggleComplete when a todo’s checkbox is clicked upon.

What you need to know

  • The can-stache-bindings Presentation’s DOM Event Bindings

  • Use on:EVENT to listen to an event on an element and call a method in can-stache. For example, the following calls doSomething() when the <div> is clicked.

    <div on:click="doSomething()"> ... </div>
    

The solution

Update index.stache to the following:

<!-- index.stache -->
<section id="todoapp">
    <header id="header">
        <h1>{{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">
            {{#each(todosList)}}
            <li class="todo {{#if(complete)}}completed{{/if}}">
                <div class="view">
                    <input class="toggle" type="checkbox"
                        {{#if(complete)}}checked{{/if}}
                        on:click="toggleComplete()"/>
                    <label>{{name}}</label>
                    <button class="destroy"></button>
                </div>
                <input class="edit" type="text" value="{{name}}"/>
            </li>
            {{/each}}
        </ul>
    </section>
    <footer id="footer" class="">
        <span id="todo-count">
            <strong>{{todosList.active.length}}</strong> items left
        </span>
        <ul id="filters">
            <li>
                <a class="selected" href="#!">All</a>
            </li>
            <li>
                <a href="#!active">Active</a>
            </li>
            <li>
                <a href="#!completed">Completed</a>
            </li>
        </ul>
        <button id="clear-completed">
            Clear completed ({{todosList.complete.length}})
        </button>
    </footer>
</section>

Toggle a todo’s completed state (data bindings)

The problem

  • Update a todo’s complete property when the checkbox’s checked property changes with two-way bindings.

What you need to know

  • The can-stache-bindings Presentation’s DOM Data Bindings

  • Use value:bind to setup a two-way binding in can-stache. For example, the following keeps name and the input’s value in sync:

    <input  value:bind="name"/>
    

The solution

Update index.stache to the following:

<!-- index.stache -->
<section id="todoapp">
    <header id="header">
        <h1>{{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">
            {{#each(todosList)}}
            <li class="todo {{#if(complete)}}completed{{/if}}">
                <div class="view">
                    <input class="toggle" type="checkbox"
                        checked:bind="complete"/>
                    <label>{{name}}</label>
                    <button class="destroy"></button>
                </div>
                <input class="edit" type="text" value="{{name}}"/>
            </li>
            {{/each}}
        </ul>
    </section>
    <footer id="footer" class="">
        <span id="todo-count">
            <strong>{{todosList.active.length}}</strong> items left
        </span>
        <ul id="filters">
            <li>
                <a class="selected" href="#!">All</a>
            </li>
            <li>
                <a href="#!active">Active</a>
            </li>
            <li>
                <a href="#!completed">Completed</a>
            </li>
        </ul>
        <button id="clear-completed">
            Clear completed ({{todosList.complete.length}})
        </button>
    </footer>
</section>

Define Todo.algebra (can-set)

The problem

  • Create a set.Algebra that understand the parameters of the /api/todos service layer. The /api/todos service layer will support the following parameters:

    • complete - Specifies a filter on todos' complete field. Examples: complete=true, complete=false.
    • sort - Specifies the sorted order the todos should be returned. Examples: sort=name.
    • id - Specifies the id property to use in /api/todos/{id}

    Example:

    GET /api/todos?complete=true&sort=name
    

Example test code:

QUnit.deepEqual( Todo.algebra.difference({}, {complete: true}), {complete: false} );
QUnit.deepEqual( Todo.algebra.clauses.id, {id: "id"} );

var sorted = Todo.algebra.getSubset({sort: "name"}, {}, [
    { name: "mow lawn", complete: false, id: 5 },
    { name: "dishes", complete: true, id: 6 },
    { name: "learn canjs", complete: false, id: 7 }
]);
QUnit.deepEqual(sorted, [
    { name: "dishes", complete: true, id: 6 },
    { name: "learn canjs", complete: false, id: 7 },
    { name: "mow lawn", complete: false, id: 5 }
]);

What you need to know

  • The can-set Presentation

  • can-set provides a way to describe the parameters used in the service layer. You use it to create a Algebra like:

    var todoAlgebra = new set.Algebra(
      set.props.boolean("completed"),
      set.props.id("_id"),
      set.props.offsetLimit("offset","limit")
    );
    

    The algebra can then be used to perform comparisons between parameters like:

    todoAlgebra.difference({}, {completed: true}) //-> {completed: false}
    
  • Use set.props to describe the behavior of your set parameters.

The solution

npm install can-set --save

Update models/todo.js to the following:

// models/todo.js
var DefineMap = require("can-define/map/");
var DefineList = require("can-define/list/");
var set = require("can-set");

var Todo = DefineMap.extend("Todo", {
    id: "string",
    name: "string",
    complete: {
        type: "boolean",
        value: false
    },
    toggleComplete: function() {
        this.complete = !this.complete;
    }
});

Todo.List = DefineList.extend("TodoList", {
    "#": Todo,
    get active() {
        return this.filter({
            complete: false
        });
    },
    get complete() {
        return this.filter({
            complete: true
        });
    },
    get allComplete() {
        return this.length === this.complete.length;
    }
});

Todo.algebra = new set.Algebra(
    set.props.boolean("complete"),
    set.props.id("id"),
    set.props.sort("sort")
);

module.exports = Todo;

Simulate the service layer (can-fixture)

The problem

Simulate a service layer that handles the following requests and responses:

GET /api/todos

-> GET /api/todos

<- {
    "data": [
      { "name": "mow lawn", "complete": false, "id": 5 },
      { "name": "dishes", "complete": true, "id": 6 },
      { "name": "learn canjs", "complete": false, "id": 7 }
    ]
}

This should also support a sort and complete params like:

-> GET /api/todos?sort=name&complete=true

GET /api/todos/{id}

-> GET /api/todos/5

<- { "name": "mow lawn", "complete": false, "id": 5 }

POST /api/todos

-> POST /api/todos
   {"name": "learn can-fixture", "complete": false}

<- {"id": 8}

PUT /api/todos/{id}

-> PUT /api/todos/8
   {"name": "learn can-fixture", "complete": true}

<- {"id": 8, "name": "learn can-fixture", "complete": true}

DELETE /api/todos/{id}

-> DELETE /api/todos/8

<- {}

What you need to know

  • The can-fixture Presentation

  • can-fixture - is used to trap AJAX requests like:

    fixture("/api/entities", function(request){
      request.data.folderId //-> "1"
    
      return {data: [...]}
    })
    
  • can-fixture.store - can be used to automatically filter records if given a Algebra.

    var entities = [ .... ];
    var entitiesStore = fixture.store( entities, entitiesAlgebra );
    fixture("/api/entities/{id}", entitiesStore);
    
    

The solution

npm install can-fixture --save

Create models/todos-fixture.js as follows:

// models/todos-fixture.js
var fixture = require("can-fixture");
var Todo = require("./todo");

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

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

module.exports = todoStore;

Connect the Todo model to the service layer (can-connect)

The problem

  • Decorate Todo with methods so it can get, create, updated, and delete todos at the /api/todos service. Specifically:
    • Todo.getList() which calls GET /api/todos
    • Todo.get({id: 5}) which calls GET /api/todos/5
    • todo.save() which calls POST /api/todos if todo doesn’t have an id or PUT /api/todos/{id} if the todo has an id.
    • todo.destroy() which calls DELETE /api/todos/5

What you need to know

  • The can-connect Presentation up to and including Migrate 2 can-connect.

  • can-connect/can/base-map/base-map can decorate a DefineMap with methods that connect it to a restful URL like:

    baseMap({
      Map: Type,
      url: "URL",
      algebra: algebra
    })
    

The solution

npm install can-connect --save

Update models/todo.js to the following:

// models/todo.js
var DefineMap = require("can-define/map/");
var DefineList = require("can-define/list/");
var set = require("can-set");
var connectBaseMap = require("can-connect/can/base-map/");

var Todo = DefineMap.extend("Todo", {
    id: "string",
    name: "string",
    complete: {
        type: "boolean",
        value: false
    },
    toggleComplete: function() {
        this.complete = !this.complete;
    }
});

Todo.List = DefineList.extend("TodoList", {
    "#": Todo,
    get active() {
        return this.filter({
            complete: false
        });
    },
    get complete() {
        return this.filter({
            complete: true
        });
    },
    get allComplete() {
        return this.length === this.complete.length;
    }
});

Todo.algebra = new set.Algebra(
    set.props.boolean("complete"),
    set.props.id("id"),
    set.props.sort("sort")
);

Todo.connection = connectBaseMap({
    url: "/api/todos",
    Map: Todo,
    List: Todo.List,
    name: "todo",
    algebra: Todo.algebra
});

module.exports = Todo;

List todos from the service layer (can-connect use)

The problem

Get all todos from the service layer using the "connected" Todo type.

What you need to know

  • The can-connect Presentation up to and including Important Interfaces.

  • Type.getList gets data using the connection’s getList and returns a promise that resolves to the Type.List of instances:

    Type.getList({}).then(function(list){
    
    })
    
  • An async getter property behavior can be used to "set" a property to an initial value:

    property: {
        get: function(lastSet, resolve) {
            SOME_ASYNC_METHOD( function callback(data) {
                resolve(data);
            });
        }
    }
    

The solution

Update index.js to the following:

// index.js
var view = require("./index.stache");
var DefineMap = require("can-define/map/");
var Todo = require("~/models/todo");
require("~/models/todos-fixture");

var AppViewModel = DefineMap.extend("AppViewModel",{
    appName: "string",
    todosList: {
        get: function(lastSet, resolve) {
            Todo.getList({}).then(resolve);
        }
    }
});

var appVM = window.appVM = new AppViewModel({
    appName: "TodoMVC"
});

var frag = view(appVM);
document.body.appendChild(frag);

require("can-todomvc-test")(appVM);

Toggling a todo’s checkbox updates service layer (can-connect use)

The problem

Update the service layer when a todo’s completed status changes. Also, disable the checkbox while the update is happening.

What you need to know

  • Call save to update a "connected" Map instance:

    map.save();
    

    save() can also be called by an on:event binding.

  • isSaving returns true when .save() has been called, but has not resolved yet.

    map.isSaving()
    

The solution

Update index.stache to the following:

<!-- index.stache -->
<section id="todoapp">
    <header id="header">
        <h1>{{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">
            {{#each(todosList)}}
            <li class="todo {{#if(complete)}}completed{{/if}}">
                <div class="view">
                    <input class="toggle" type="checkbox"
                        checked:bind="complete"
                        on:change="save()"
                        disabled:from="isSaving()"/>
                    <label>{{name}}</label>
                    <button class="destroy"></button>
                </div>
                <input class="edit" type="text" value="{{name}}"/>
            </li>
            {{/each}}
        </ul>
    </section>
    <footer id="footer" class="">
        <span id="todo-count">
            <strong>{{todosList.active.length}}</strong> items left
        </span>
        <ul id="filters">
            <li>
                <a class="selected" href="#!">All</a>
            </li>
            <li>
                <a href="#!active">Active</a>
            </li>
            <li>
                <a href="#!completed">Completed</a>
            </li>
        </ul>
        <button id="clear-completed">
            Clear completed ({{todosList.complete.length}})
        </button>
    </footer>
</section>

Delete todos in the page (can-connect use)

The problem

When a todo’s destroy button is clicked, we need to delete the todo on the server and remove the todo’s element from the page. While the todo is being destroyed, add destroying to the todo’s <li>’s class attribute.

Things to know

  • The remaining parts of the can-connect Presentation, with an emphasis on how real-time behavior works.

  • Delete a record on the server with destroy like:

    map.destroy()
    
  • isDestroying returns true when .destroy() has been called, but has not resolved yet.

    map.isDestroying()
    

The solution

Update index.stache to the following:

<!-- index.stache -->
<section id="todoapp">
    <header id="header">
        <h1>{{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">
            {{#each(todosList)}}
            <li class="todo {{#if(complete)}}completed{{/if}}
                {{#if(isDestroying)}}destroying{{/if}}">
                <div class="view">
                    <input class="toggle" type="checkbox"
                        checked:bind="complete"
                        on:change="save()"
                        disabled:from="isSaving()"/>
                    <label>{{name}}</label>
                    <button class="destroy" on:click="destroy()"></button>
                </div>
                <input class="edit" type="text" value="{{name}}"/>
            </li>
            {{/each}}
        </ul>
    </section>
    <footer id="footer" class="">
        <span id="todo-count">
            <strong>{{todosList.active.length}}</strong> items left
        </span>
        <ul id="filters">
            <li>
                <a class="selected" href="#!">All</a>
            </li>
            <li>
                <a href="#!active">Active</a>
            </li>
            <li>
                <a href="#!completed">Completed</a>
            </li>
        </ul>
        <button id="clear-completed">
            Clear completed ({{todosList.complete.length}})
        </button>
    </footer>
</section>

Create todos (can-component)

The problem

Make it possible to create a todo, update the service layer and show the todo in the list of todos.

This functionality should be encapsulated by a <todo-create/> custom element.

What you need to know

  • The can-component presentation up to and including how to define a component.

  • A can-component combines a custom tag name, can-stache view and a can-define/map/map ViewModel like:

    var Component = require("can-component");
    var view = require("./template.stache");
    var ViewModel = DefineMap.extend({
      ...      
    });
    
    Component.extend({
        tag: "some-component",
        view: view,
        ViewModel: ViewModel
    });
    
  • You can use on:enter to listen to when the user hits the enter key.

  • The Value behavior creates a default value by using new Value to initialize the value when a DefineMap property is read for the first time.

    var SubType = DefineMap.extend({})
    var Type = DefineMap.extend({
        property: {Value: SubType}
    })
    
    var map = new Type();
    map.property instanceof SubType //-> true
    
  • Use can-view-import to import a module from a template like:

    <can-import from="~/components/some-component/"/>
    <some-component>
    

The solution

npm install can-component --save

Create components/todo-create/todo-create.stache as follows:

<!-- components/todo-create/todo-create.stache -->
<input id="new-todo"
    placeholder="What needs to be done?"
    value:bind="todo.name"
    on:enter="createTodo()"/>

Create components/todo-create/todo-create.js as follows:

// components/todo-create/todo-create.js
var Component = require("can-component"); // remember to install
var DefineMap = require("can-define/map/");
var view = require("./todo-create.stache");
var Todo = require("~/models/todo");

var TodoCreateVM = DefineMap.extend({
    todo: {
        Value: Todo
    },
    createTodo: function() {
        this.todo.save().then(function() {
            this.todo = new Todo();
        }.bind(this));
    }
});

module.exports = Component.extend({
    tag: "todo-create",
    view: view,
    ViewModel: TodoCreateVM
});

Update index.stache to the following:

<!-- index.stache -->
<can-import from="~/components/todo-create/"/>
<section id="todoapp">
    <header id="header">
        <h1>{{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">
            {{#each(todosList)}}
            <li class="todo {{#if(complete)}}completed{{/if}}
                {{#if(isDestroying)}}destroying{{/if}}">
                <div class="view">
                    <input class="toggle" type="checkbox"
                        checked:bind="complete"
                        on:change="save()"
                        disabled:from="isSaving()"/>
                    <label>{{name}}</label>
                    <button class="destroy" on:click="destroy()"></button>
                </div>
                <input class="edit" type="text" value="{{name}}"/>
            </li>
            {{/each}}
        </ul>
    </section>
    <footer id="footer" class="">
        <span id="todo-count">
            <strong>{{todosList.active.length}}</strong> items left
        </span>
        <ul id="filters">
            <li>
                <a class="selected" href="#!">All</a>
            </li>
            <li>
                <a href="#!active">Active</a>
            </li>
            <li>
                <a href="#!completed">Completed</a>
            </li>
        </ul>
        <button id="clear-completed">
            Clear completed ({{todosList.complete.length}})
        </button>
    </footer>
</section>

Edit todo names (can-stache-bindings)

The problem

Make it possible to edit a todos name by double-clicking its label which should reveal a focused input element. If the user hits the enter key, the todo should be updated on the server. If the input loses focus, it should go back to the default list view.

This functionality should be encapsulated by a <todo-list {todos}/> custom element. It should accept a todos property that is the list of todos that will be managed by the custom element.

What you need to know

  • The can-stache-bindings presentation on data bindings.

  • The focused custom attribute can be used to specify when an element should be focused:

    focused:from="shouldBeFocused()"
    
  • Use toChild:from to pass a value from the scope to a component:

    <some-component {name-in-component}="nameInScope"/>
    
  • this can be used to get the current context in stache:

    <div on:click="doSomethingWith(this)"/>
    

The solution

Create components/todo-list/todo-list.stache as follows:

<!-- components/todo-list/todo-list.stache -->
<ul id="todo-list">
    {{#each(todos)}}
    <li class="todo {{#if(./complete)}}completed{{/if}}
        {{#if(isDestroying)}}destroying{{/if}}
        {{#if(isEditing(this))}}editing{{/if}}">
        <div class="view">
            <input class="toggle" type="checkbox"
                checked:bind="complete"
                on:change="save()"
                disabled:from="isSaving()"/>
            <label on:dblclick="edit(this)">{{name}}</label>
            <button class="destroy" on:click="destroy()"></button>
        </div>
        <input class="edit" type="text"
            value:bind="name"
            on:enter="updateName()"
            focused:from="isEditing(this)"
            on:blur="cancelEdit()"/>
    </li>
    {{/each}}
</ul>

Create components/todo-list/todo-list.js as follows:

// components/todo-list/todo-list.js
var Component = require("can-component");
var DefineMap = require("can-define/map/");
var view = require("./todo-list.stache");
var Todo = require("~/models/todo");

var TodoListVM = DefineMap.extend({
    todos: Todo.List,
    editing: Todo,
    backupName: "string",
    isEditing: function(todo) {
        return todo === this.editing;
    },
    edit: function(todo) {
        this.backupName = todo.name;
        this.editing = todo;
    },
    cancelEdit: function() {
        if (this.editing) {
            this.editing.name = this.backupName;
        }
        this.editing = null;
    },
    updateName: function() {
        this.editing.save();
        this.editing = null;
    }
});

module.exports = Component.extend({
    tag: "todo-list",
    view: view,
    ViewModel: TodoListVM
});

Update index.stache to the following:

<!-- index.stache -->
<can-import from="~/components/todo-create/"/>
<can-import from="~/components/todo-list/"/>
<section id="todoapp">
    <header id="header">
        <h1>{{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="todosList"/>
    </section>
    <footer id="footer" class="">
        <span id="todo-count">
            <strong>{{todosList.active.length}}</strong> items left
        </span>
        <ul id="filters">
            <li>
                <a class="selected" href="#!">All</a>
            </li>
            <li>
                <a href="#!active">Active</a>
            </li>
            <li>
                <a href="#!completed">Completed</a>
            </li>
        </ul>
        <button id="clear-completed">
            Clear completed ({{todosList.complete.length}})
        </button>
    </footer>
</section>

Toggle all todos complete state (DefineMap setter)

The problem

Make the “toggle all” checkbox work. It should be unchecked if a single todo is unchecked and checked if all todos are checked.

When the “toggle all” checkbox is changed, the application should update every todo to match the status of the “toggle all” checkbox.

The “toggle all” checkbox should be disabled if a single todo is saving.

What you need to know

  • Using setters and getters a virtual property can be simulated like:

    DefineMap.extend({
        first: "string",
        last: "string",
        get fullName(){
            return this.first + " " + this.last;
        },
        set fullName(newValue){
            var parts = newValue.split(" ");
            this.first = parts[0];
            this.last = parts[1];
        }
    })
    

The solution

Update models/todo.js to the following:

// models/todo.js
var DefineMap = require("can-define/map/");
var DefineList = require("can-define/list/");
var set = require("can-set");
var connectBaseMap = require("can-connect/can/base-map/");

var Todo = DefineMap.extend("Todo", {
    id: "string",
    name: "string",
    complete: {
        type: "boolean",
        value: false
    },
    toggleComplete: function() {
        this.complete = !this.complete;
    }
});

Todo.List = DefineList.extend("TodoList", {
    "#": Todo,
    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: function(value) {
        this.forEach(function(todo) {
            todo.complete = value;
            todo.save();
        });
    }
});

Todo.algebra = new set.Algebra(
    set.props.boolean("complete"),
    set.props.id("id"),
    set.props.sort("sort")
);

Todo.connection = connectBaseMap({
    url: "/api/todos",
    Map: Todo,
    List: Todo.List,
    name: "todo",
    algebra: Todo.algebra
});

module.exports = Todo;

Update index.js to the following:

// index.js
var view = require("./index.stache");
var DefineMap = require("can-define/map/");
var Todo = require("~/models/todo");
require("~/models/todos-fixture");

var AppViewModel = DefineMap.extend("AppViewModel",{
    appName: "string",
    todosList: {
        get: function(lastSet, resolve) {
            Todo.getList({}).then(resolve);
        }
    },
    get allChecked() {
        return this.todosList && this.todosList.allComplete;
    },
    set allChecked(newVal) {
        this.todosList && this.todosList.updateCompleteTo(newVal);
    }
});

var appVM = window.appVM = new AppViewModel({
    appName: "TodoMVC"
});

var frag = view(appVM);
document.body.appendChild(frag);

require("can-todomvc-test")(appVM);

Update index.stache to the following:

<!-- index.stache -->
<can-import from="~/components/todo-create/"/>
<can-import from="~/components/todo-list/"/>
<section id="todoapp">
    <header id="header">
        <h1>{{appName}}</h1>
        <todo-create/>
    </header>
    <section id="main" class="">
        <input id="toggle-all" type="checkbox"
            checked:bind="allChecked"
            disabled:from="todosList.saving.length"/>
        <label for="toggle-all">Mark all as complete</label>
        <todo-list todos:from="todosList"/>
    </section>
    <footer id="footer" class="">
        <span id="todo-count">
            <strong>{{todosList.active.length}}</strong> items left
        </span>
        <ul id="filters">
            <li>
                <a class="selected" href="#!">All</a>
            </li>
            <li>
                <a href="#!active">Active</a>
            </li>
            <li>
                <a href="#!completed">Completed</a>
            </li>
        </ul>
        <button id="clear-completed">
            Clear completed ({{todosList.complete.length}})
        </button>
    </footer>
</section>

Clear completed todo’s (event bindings)

The problem

Make the "Clear completed" button work. When the button is clicked, It should destroy each completed todo.

What you need to know

  • The can-stache-bindings Presentation’s DOM Event Bindings

  • Use on:EVENT to listen to an event on an element and call a method in can-stache. For example, the following calls doSomething() when the <div> is clicked.

    <div on:click="doSomething()"> ... </div>
    

The solution

Update models/todo.js to the following:

// models/todo.js
var DefineMap = require("can-define/map/");
var DefineList = require("can-define/list/");
var set = require("can-set");
var connectBaseMap = require("can-connect/can/base-map/");

var Todo = DefineMap.extend("Todo", {
    id: "string",
    name: "string",
    complete: {
        type: "boolean",
        value: false
    },
    toggleComplete: function() {
        this.complete = !this.complete;
    }
});

Todo.List = DefineList.extend("TodoList", {
    "#": Todo,
    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: function(value) {
        this.forEach(function(todo) {
            todo.complete = value;
            todo.save();
        });
    },
    destroyComplete: function(){
        this.complete.forEach(function(todo){
            todo.destroy();
        });
    }
});

Todo.algebra = new set.Algebra(
    set.props.boolean("complete"),
    set.props.id("id"),
    set.props.sort("sort")
);

Todo.connection = connectBaseMap({
    url: "/api/todos",
    Map: Todo,
    List: Todo.List,
    name: "todo",
    algebra: Todo.algebra
});

module.exports = Todo;

Update index.stache to the following:

<!-- index.stache -->
<can-import from="~/components/todo-create/"/>
<can-import from="~/components/todo-list/"/>
<section id="todoapp">
    <header id="header">
        <h1>{{appName}}</h1>
        <todo-create/>
    </header>
    <section id="main" class="">
        <input id="toggle-all" type="checkbox"
            checked:bind="allChecked"
            disabled:from="todosList.saving.length"/>
        <label for="toggle-all">Mark all as complete</label>
        <todo-list todos:from="todosList"/>
    </section>
    <footer id="footer" class="">
        <span id="todo-count">
            <strong>{{todosList.active.length}}</strong> items left
        </span>
        <ul id="filters">
            <li>
                <a class="selected" href="#!">All</a>
            </li>
            <li>
                <a href="#!active">Active</a>
            </li>
            <li>
                <a href="#!completed">Completed</a>
            </li>
        </ul>
        <button id="clear-completed"
            on:click="allTodos.destroyComplete()">
            Clear completed ({{allTodos.complete.length}})
        </button>>
    </footer>
</section>

Setup routing (can-route)

Make it so that the following urls display the corresponding todos:

  • #! or - All todos
  • #!active - Only the incomplete todos
  • #!complete - Only the completed todos

Also, the All, Active, and Completed buttons should link to those pages and a class='selected' property should be added if they represent the current page.

What you need to know

  • can-route is used to connect a DefineMap’s properties to the URL. This is done with data like:

    route.data = new AppViewModel();
    
  • can-route can create pretty routing rules. For example, if #!login should set the page property of the AppViewModel to "login", use route() like:

    route("{page}");
    
  • ready initializes the connection between the url and the AppViewModel. After you've created all your application’s pretty routing rules, call it like:

    route.ready()
    
  • The can-stache/helpers/route module provides helpers that use can-route.

    {{#routeCurrent(hash)}} returns truthy if the current route matches its first parameters properties.

    {{#if(routeCurrent(page='login',true))}}
      You are on the login page.
    {{/if}}
    

    {{routeUrl(hashes)}} returns a url that will set its first parameters properties:

    <a href="{{routeUrl(page='login')}}">Login</a>
    

The solution

npm install can-route --save

Update index.js to the following:

// index.js
var view = require("./index.stache");
var DefineMap = require("can-define/map/");
var Todo = require("~/models/todo");
var route = require("can-route");
require("~/models/todos-fixture");

var AppViewModel = DefineMap.extend("AppViewModel", {
    appName: {type: "string", serialize: false},
    filter: "string",
    allTodos: {
        get: function(lastSet, resolve) {
            Todo.getList({}).then(resolve);
        }
    },
    get todosList() {
        if(this.allTodos) {
            if(this.filter === "complete") {
                return this.allTodos.complete;
            } else if(this.filter === "active") {
                return this.allTodos.active;
            } else {
                return this.allTodos;
            }
        }
    },
    get allChecked() {
        return this.todosList && this.todosList.allComplete;
    },
    set allChecked(newVal) {
        this.todosList && this.todosList.updateCompleteTo(newVal);
    }
});

var appVM = window.appVM = new AppViewModel({
    appName: "TodoMVC"
});

route.data = appVM;
route("{filter}");
route.ready();

var frag = view(appVM);
document.body.appendChild(frag);

require("can-todomvc-test")(appVM);

Update index.stache to the following:

<!-- index.stache -->
<can-import from="~/components/todo-create/"/>
<can-import from="~/components/todo-list/"/>
<can-import from="can-stache/helpers/route"/>
<section id="todoapp">
    <header id="header">
        <h1>{{appName}}</h1>
        <todo-create/>
    </header>
    <section id="main" class="">
        <input id="toggle-all" type="checkbox"
          checked:bind="allChecked"
          disabled:from="todosList.saving.length"/>
        <label for="toggle-all">Mark all as complete</label>
        <todo-list todos:from="todosList"/>
    </section>
    <footer id="footer" class="">
        <span id="todo-count">
            <strong>{{allTodos.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="allTodos.destroyComplete()">
            Clear completed ({{allTodos.complete.length}})
        </button>
    </footer>
</section>

Success! You’ve completed this guide. Have questions or comments? Let us know on Gitter chat or our forums!

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

On this page

Get help

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