DoneJS StealJS jQuery++ FuncUnit DocumentJS
6.6.1
5.33.3 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
        • Introduction
        • Configuring Requests
        • Managing Sessions
        • Customizing Connections
      • 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
      • Weather Report
    • 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

Introduction

  • Edit on GitHub

Learn how to get, create, update, and delete backend service layer data.

Overview

Most applications need to request data from a server. For example, you might have used XMLHttpRequest to make an AJAX request, used the new fetch API to get JSON data, or used jQuery.ajax to post form data.

While these techniques are fine in a CanJS application, using CanJS’s service-layer modeling tools can solve some difficult problems with little configuration:

  • Provide a standard interface for retrieving, creating, updating, and deleting data.
  • Convert raw data from the server to typed data, with methods and special property behaviors.
  • Caching
  • Real time updates of instances and lists
  • Prevent multiple instances of a given id or multiple lists of a given set from being created.
  • Handle relationships between data.

This guide walks you through the basics of CanJS’s service layer modeling.

Creating a model

CanJS’s pattern is such that you define application logic in one or more observables, then connect the observables to various browser APIs. CanJS’s model layer libraries connect observables to backend services. CanJS’s model-layer will make AJAX requests to get, create, update, and delete data.

For example, you can connect a Todo and TodoList observable to a restful service layer at /api/todos with can-rest-model:

import { ObservableArray, ObservableObject, restModel, type } from "can";

class Todo extends ObservableObject {
    static props = {
        // `id` uniquely identifies instances of this type.
        id: { type: type.maybe(Number), identity: true },

        // properties on models can be given types as well as default values
        complete: { type: Boolean, default: false },
        dueDate: type.maybeConvert(Date),
        name: type.maybe(String)
    };

    toggleComplete() {
        this.complete = !this.complete;
    }
}

class TodoList extends ObservableArray {
    static items = Todo;  

    completeAll() {
        return this.forEach((todo) => { todo.complete = true; });
    }
}

const todoConnection = restModel({
    ObjectType: Todo,
    ArrayType: TodoList,
    url: "/api/todos/{id}"
});

This allows you to get, create, update and destroy data programmatically through the Todo observable. can-rest-model mixes in the following methods:

  • Todo.getList({filter: {complete: true}}) - GET a list of todos from /api/todos?filter[complete]=true.
  • Todo.get({id: 5}) - GET a single todo from /api/todos/5.
  • new Todo({name: "Learn CanJS"}).save() - Create a todo by POSTing it to /api/todos.
  • todo.save() - Update a todo's data by PUTing it to /api/todos/5.
  • todo.destroy() - Delete a todo by DELETE-ing it from /api/todos/5.

The following sections show examples of how to use these methods.

Retrieving a list of records

Use getList to retrieve records.

JavaScript API Request Response

Call .getList with parameters used to filter, sort, and paginate your list.

The parameters are serialized with can-param and added to the restful url.

Server responds with data property containing the records. Configure for other formats with parseListProp or parseListData.

Todo.getList({
  filter: {
    complete: false
  }
}) //-> Promise<TodoList[]>






GET /api/todos?
  filter[complete]=false









{
  "data": [
    {
      "id": 20,
      "name": "mow lawn",
      "complete": false
    },
    ...
  ]
}

.getList(params) returns a Promise that will eventually resolve to a TodoList of Todo instances. This means that properties and methods defined on TodoList and Todo will be available:

let todosPromise = Todo.getList({});

todosPromise.then(function(todos){
    todos    //-> Todos.List[Todo..]
    todos[0] //-> Todo{id: 1, name: "Learn CanJS", complete: false, ...}

    todos.completeAll();
    todos[0] //-> Todo{id: 1, name: "Learn CanJS", complete: true, ...}

    todos[0].toggleComplete();
    todos[0] //-> Todo{id: 1, name: "Learn CanJS", complete: false, ...}
})

The following creates a component that uses Todo.getList to load and list data:

class TodoList extends StacheElement {
    static view = `
        <ul>
            {{# if(this.todosPromise.isResolved) }}
                {{# for(todo of this.todosPromise.value) }}
                    <li>
                        <input type='checkbox' checked:bind='todo.complete' disabled/>
                        <label>{{ todo.name }}</label>
                        <input type='date' valueAsDate:bind='todo.dueDate' disabled/>
                    </li>
                {{/ for }}
            {{/ if }}
            {{# if(this.todosPromise.isPending) }}
                <li>Loading</li>
            {{/ if }}
        </ul>
    `;

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

Note: A promise's values and state can be read in can-stache directly via: promise.value, promise.reason, promise.isResolved, promise.isPending, and promise.isRejected.

See the component in action here:

The object passed to .getList can be used to filter, sort, and paginate the items retrieved. The following adds inputs to control the filtering, sorting, and pagination of the component:

class TodoList extends StacheElement {
    static view = `
        Sort By: <select value:bind="sort">
            <option value="">none</option>
            <option value="name">name</option>
            <option value="dueDate">dueDate</option>
        </select>

        Show: <select value:bind="completeFilter">
            <option value="">All</option>
            <option value="complete">Complete</option>
            <option value="incomplete">Incomplete</option>
        </select>

        Due: <select value:bind="dueFilter">
            <option value="">Anytime</option>
            <option value="today">Today</option>
            <option value="week">This Week</option>
        </select>

        Results <select value:bind="count">
            <option value="">All</option>
            <option value="10">10</option>
            <option value="20">20</option>
        </select>

        <ul>
            {{# if(this.todosPromise.isResolved) }}
                {{# for(todo of this.todosPromise.value) }}
                    <li>
                        <input type='checkbox' checked:bind='todo.complete' disabled/>
                        <label>{{ todo.name }}</label>
                        <input type='date' valueAsDate:bind='todo.dueDate' disabled/>
                    </li>
                {{/ for }}
            {{/ if }}
            {{# if(this.todosPromise.isPending) }}
                <li>Loading</li>
            {{/ if }}
        </ul>
    `;

    static props = {
        sort: type.maybe(String),
        completeFilter: type.maybe(String),
        dueFilter: type.maybe(String),
        count: {type: String, default: "10"},
        get todosPromise(){
            let query = {filter: {}};
            if(this.sort) {
                query.sort =  this.sort;
            }
            if(this.completeFilter) {
                query.filter.complete = this.completeFilter === "complete";
            }
            if(this.dueFilter) {
                let day = 24*60*60*1000;
                let now = new Date();
                let today = new Date(now.getFullYear(), now.getMonth(), now.getDate() );
                if(this.dueFilter === "today") {

                    query.filter.dueDate = {
                        $gte: now.toString(),
                        $lt: new Date(now.getTime() + day).toString()
                    }
                }
                if(this.dueFilter === "week") {
                    let start = today.getTime() - (today.getDay() * day);
                    query.filter.dueDate = {
                        $gte: new Date(start).toString(),
                        $lt: new Date(start + 7*day).toString()
                    };
                }
            }
            if(this.count) {
                query.page = {
                    start: 0,
                    end: (+this.count)-1
                };
            }
            return Todo.getList(query);
        }
    };
};
customElements.define("todo-list", TodoList);

See it in action here:

Creating records

Use save to create records.

JavaScript API Request Response

Create an instance and then call .save() to create a record.

The instance is serialized and POSTed to the server.

Server responds with the identity value and all other values on the record. Use updateInstanceWithAssignDeep to not require every record value.

const todo = new Todo({
  name: "make a model"
})
todo.save()  //-> Promise<Todo>


POST /api/todos
    {
      "name": "make a model",
      "complete": false
    }
{
  "id": 22,
  "name": "make a model",
  "complete": false
}

.save() returns a Promise that eventually resolves to the same instance that .save() was called on. While the record is being saved, isSaving will return true:

const todo = new Todo({
  name: "make a model"
});

todo.isSaving() //-> false

todoPromise = todo.save();

todo.isSaving() //-> true

todoPromise.then(function(){
    todo.isSaving() //-> false
});

The following creates a component that uses Todo.prototype.save to create data:

class TodoCreate extends StacheElement {
    static view = `
        <form on:submit="createTodo(scope.event)">
            <p>
                <label>Name</label>
                <input on:input:value:bind='todo.name'/>
            </p>
            <p>
                <label>Complete</label>
                <input type='checkbox' checked:bind='todo.complete'/>
            </p>
            <p>
                <label>Date</label>
                <input type='date' valueAsDate:bind='todo.dueDate'/>
            </p>
            <button disabled:from="todo.preventSave()">Create Todo</button>
            {{# if(todo.isSaving()) }}Creating…{{/ if }}
        </form>
    `;

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

    createTodo(event) {
        event.preventDefault();
        this.todo.save().then((createdTodo) => {
            // Create a new todo instance to receive from data
            this.todo = new Todo();
        })
    }
};
customElements.define("todo-create", TodoCreate);

See this component in action here:

Note that this demo lists newly created todos by listening to Todo’s created event as follows:

class CreatedTodos extends StacheElement {
    static view = `
        <h3>Created Todos</h3>
        <table>
            <tr>
                <th>id</th><th>complete</th>
                <th>name</th><th>due date</th>
            </tr>
            {{# for(todo of this.todos) }}
                <tr>
                    <td>{{todo.id}}</td>
                    <td><input type='checkbox' checked:bind='todo.complete' disabled/></td>
                    <td>{{todo.name}}</td>
                    <td><input type='date' valueAsDate:bind='todo.dueDate' disabled/></td>
                </tr>
            {{ else }}
                <tr><td colspan='4'><i>The todos you create will be listed here</i></td></tr>
            {{/ for }}
        </table>
    `;  

    static props = {
        todos: {Default: TodoList}
    };    

    connected() {
        this.listenTo(Todo,"created", (event, created) => {
            this.todos.unshift(created);
        })
    }
};
customElements.define("created-todos", CreatedTodos);

When any todo is created, updated, or destroyed, an event is dispatched on the Todo type.

Updating records

Also use save to update records.

JavaScript API Request Response

On an instance that has already been created, change its data and call .save() to update the record.

The instance is serialized and PUT to the server.

Server responds with all values on the record. Use updateInstanceWithAssignDeep to not require every record value.

todo.complete = true;
todo.save()  //-> Promise<Todo>




PUT /api/todos/22
    {
      "name": "make a model",
      "complete": true
    }
{
  "id": 22,
  "name": "make a model",
  "complete": true
}

For example, the following creates a component that uses Todo.prototype.save to update data:

class TodoUpdate extends StacheElement {
    static view = `
        {{# if(todo) }}
            <h3>Update Todo</h3>
            <form on:submit="updateTodo(scope.element, scope.event)">
                <p>
                    <label>Name</label>
                    <input name="name" value:from='todo.name' />
                </p>
                <p>
                    <label>Complete</label>
                    <input type='checkbox' name='complete'
                        checked:from='todo.complete'/>
                </p>
                <p>
                    <label>Date</label>
                    <input type='date'
                        name='dueDate' valueAsDate:from='todo.dueDate'/>
                </p>
                <button disabled:from="todo.preventSave()">
                    {{# if(todo.isSaving()) }}Updating{{else}}Update{{/ if }}Todo
                </button>
                <button disabled:from="todo.preventSave()"
                    on:click="cancelEdit()">Cancel</button>

            </form>
        {{ else }}
            <i>Click a todo above to edit it here.</i>
        {{/ if }}
    `;

    static props = {
        todo: type.maybeConvert(Todo)
    };

    updateTodo(form, event) {
        event.preventDefault();
        this.todo.assign({
            name: form.name.value,
            complete: form.complete.checked,
            dueDate: form.dueDate.valueAsDate
        }).save().then(this.cancelEdit.bind(this))
    }

    cancelEdit() {
        this.todo = null;
    }
}
customElements.define("todo-update", TodoUpdate);

See this in action here:

Destroying records

Use destroy to delete records.

JavaScript API Request Response

On an instance that has already been created, call .destroy() to delete the record.

A DELETE request is sent with the instance's identity.

No response data is necessary. Just a successful status code.

todo.destroy()  //-> Promise<Todo>
DELETE /api/todos/22
200 Status code.

The following creates a component that uses Todo.prototype.destroy to delete data:

class TodoList extends StacheElement {
    static view = `
        <ul>
            {{# if(this.todosPromise.isResolved) }}
                {{# for(todo of this.todosPromise.value) }}
                    <li>
                        <input type='checkbox' checked:bind='todo.complete' disabled/>
                        <label>{{ todo.name }}</label>
                        <input type='date' valueAsDate:bind='todo.dueDate' disabled/>
                        <button on:click="todo.destroy()">delete</button>
                    </li>
                {{/ for }}
            {{/ if }}
            {{# if(this.todosPromise.isPending) }}
                <li>Loading</li>
            {{/ if }}
        </ul>
    `;

    static props = {
        todosPromise: {
            get default() {
                return Todo.getList({});
            }
        }
    };

    connected() {
        this.todosPromise.then((todos)=>{
            this.listenTo(Todo, "destroyed", function(ev, destroyed){
                let index = todos.indexOf(destroyed);
                todos.splice(index, 1);
            });
        });
    }
};
customElements.define("todo-list", TodoList);

The following example shows this in action. Click the button to delete todos and have the todo removed from the list.

This demo works by calling destroy when the button is clicked.

<button on:click="destroy()">delete</button>

To keep the list of todos up to date, the above demo works by listening when any todo is destroyed and removing it from the list:

connected(){
    this.todosPromise.then((todos)=>{
        this.listenTo(Todo, "destroyed", function(ev, destroyed){
            let index = todos.indexOf(destroyed);
            todos.splice(index, 1);
        });
    });
}

Update lists when records are mutated

The previous Creating Records, Updating Records and Destroying Records examples showed how to listen to when records are mutated:

this.listenTo(Todo,"created",   (event, createdTodo)   => { /* ... */ })
this.listenTo(Todo,"updated",   (event, updatedTodo)   => { /* ... */ })
this.listenTo(Todo,"destroyed", (event, destroyedTodo) => { /* ... */ })

These listeners can be used to update lists similar to how the Destroying Records example removed lists:

connected(){
    this.todosPromise.then( (todos)=>{
        this.listenTo(Todo, "created", function(ev, created){
            // ADD created to `todos`
        })
        this.listenTo(Todo, "destroyed", function(ev, destroyed){
            // REMOVE destroyed from `todos`
        });
        this.listenTo(Todo, "updated", function(ev, updated){
            // ADD, REMOVE, or UPDATE the position of updated
            // within `todos`
        });
    });
}

But this is cumbersome, especially when lists contain sorted and filtered results. For example, if you are displaying only completed todos, you might not want to add newly created incomplete todos. The following only pushes complete todos onto todos:

static props = {
    todosPromise: {
        default(){
            return Todo.getList({filter: {complete: true}});
        }
    },
}
connected(){
    this.todosPromise.then((todos) => {
        this.listenTo(Todo, "created", (ev, createdTodo) => {
            // make sure the todo is complete:
            if(createdTodo.complete) {
                todos.push(complete);
            }
        });
    });
}

Fortunately, can-realtime-rest-model using can-query-logic can automatically update lists for you! If your service layer matches what can-query-logic expects, you can just replace can-rest-model with can-realtime-rest-model as follows:

import { ObservableArray, ObservableObject, realtimeRestModel, type } from "can";

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

class TodoList extends ObservableArray {
    static items = Todo;
}

const todoConnection = realtimeRestModel({
    ObjectType: Todo,
    ArrayType: TodoList,
    url: "/api/todos/{id}"
});

Note: You can configure can-query-logic to match your service layer. Learn more in the configuration section of can-query-logic.

The following uses can-realtime-rest-model to create a filterable and sortable grid that automatically updates itself when todos are created, updated or destroyed.

Try out the following use cases that can-realtime-rest-model provides automatically:

  • Delete a todo and the todo will be removed from the list.
  • Sort by date, then create a todo and the todo will be inserted into the right place in the list.
  • Sort by date, then edit a todo's dueDate and the todo will be moved to the right place in the list.
  • Show only Complete todos, then toggle the todo's complete status and the todo will be removed from the view.

By default, can-query-logic assumes your service layer will match a default query structure that looks like:

Todo.getList({
    // Selects only the todos that match.
    filter: {
        complete: {$in: [false, null]}
    },
    // Sort the results of the selection
    sort: "-name",
    // Selects a range of the sorted result
    page: {start: 0, end: 19}
})

This structures follows the Fetching Data JSONAPI specification.

There's a:

  • filter property for filtering records,
  • sort property for specifying the order to sort records, and
  • page property that selects a range of the sorted result. The range indexes are inclusive.

NOTE: can-realtime-rest-model does not follow the rest of the JSONAPI specification. Specifically can-realtime-rest-model expects your server to send back JSON data in a different format.

If you control the service layer, we encourage you to make it match the default query structure to avoid configuration. The default query structure also supports the following Comparison Operators: $eq, $gt, $gte, $in, $lt, $lte, $ne, $nin.

If you are unable to match the default query structure, or need special behavior, read the configuration section of can-query-logic to learn how to configure a custom query logic.

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

On this page

Get help

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