Introduction
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 |
The parameters are serialized with can-param and added to the restful url. |
Server responds with |
|
|
|
.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
, andpromise.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 |
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. |
|
|
|
.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 |
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. |
|
|
|
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 |
A DELETE request is sent with the instance's identity. |
No response data is necessary. Just a successful 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.