DoneJS StealJS jQuery++ FuncUnit DocumentJS
5.33.3
6.0.0 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
  • API Docs
    • Observables
      • can-bind
      • can-compute
      • can-debug
      • can-define
      • can-define/list/list
      • can-define/map/map
      • can-define-backup
      • can-define-stream
      • can-define-stream-kefir
      • can-event-queue
      • can-kefir
      • can-list
      • can-map
      • can-map-compat
      • can-map-define
      • can-observable-array
      • can-observable-object
      • can-observation
      • can-observation-recorder
      • can-observe
      • can-simple-map
      • can-simple-observable
      • can-stream
      • can-stream-kefir
      • can-value
    • Views
      • can-attribute-observable
      • can-component
      • can-stache
      • can-stache-bindings
      • can-stache-converters
      • can-stache-element
      • can-stache-route-helpers
      • can-view-autorender
      • can-view-callbacks
      • can-view-import
      • can-view-live
      • can-view-model
      • can-view-nodelist
      • can-view-parser
      • can-view-scope
      • can-view-target
      • steal-stache
    • Data Modeling
      • can-connect
      • can-connect-feathers
      • can-connect-ndjson
      • can-connect-tag
      • can-fixture
      • can-fixture-socket
      • can-local-store
      • can-memory-store
      • can-ndjson-stream
      • can-query-logic
      • can-realtime-rest-model
      • can-rest-model
      • can-set-legacy
      • can-super-model
    • Routing
      • can-deparam
      • can-param
      • can-route
      • can-route-hash
      • can-route-mock
      • can-route-pushstate
    • JS Utilities
      • can-assign
      • can-define-lazy-value
      • can-diff
      • can-globals
      • can-join-uris
      • can-key
      • can-key-tree
      • can-make-map
      • can-parse-uri
      • can-queues
      • can-string
      • can-string-to-any
      • can-zone-storage
    • DOM Utilities
      • can-ajax
      • can-attribute-encoder
      • can-child-nodes
      • can-control
      • can-dom-data
      • can-dom-events
      • can-dom-mutate
      • can-event-dom-enter
      • can-event-dom-radiochange
      • can-fragment
    • Data Validation
      • can-define-validate-validatejs
      • can-type
      • can-validate
      • can-validate-interface
      • can-validate-legacy
      • can-validate-validatejs
    • Typed Data
      • can-cid
      • can-construct
      • can-construct-super
      • can-data-types
      • can-namespace
      • can-reflect
      • can-reflect-dependencies
      • can-reflect-promise
      • can-types
    • Polyfills
      • can-symbol
      • can-vdom
    • Core
    • Infrastructure
      • can-global
      • can-test-helpers
    • Ecosystem
    • Legacy
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

can-rest-model

  • npm package badge
  • Star
  • Edit on GitHub

Connect a type to a restful service layer.

restModel(options)

restModel extends the provided options.Map type with the ability to connect to a restful service layer. For example, the following extends a Todo type with the ability to connect to a restful service layer:

import {Todo, todoFixture} from "//unpkg.com/can-demo-models@5";
import {restModel} from "can";

// Creates a mock backend with 5 todos
todoFixture(5);

Todo.connection = restModel({
    Map: Todo,
    List: Todo.List,
    url: "/api/todos/{id}"
});

// Prints out all todo names
Todo.getList().then(todos => {
    todos.forEach(todo => {
        console.log(todo.name);
    })
})

restModel mixes in the following behaviors:

  • constructor
  • can/map
  • can-connect/data/parse/parse
  • data/url
  • base

Parameters

  1. options {Object}:

    Configuration options supported by all the mixed-in behaviors:

    • Map - The map type constructor function used to create instances of the raw record data retrieved from the server. The type will also be decorated with the following methods:

      • getList
      • get
      • save
      • destroy
      • isSaving
      • isDestroying
      • isNew
    • List - The list type constructor function used to create a list of instances of the raw record data retrieved from the server. _

    • url - Configure the URLs used to create, retrieve, update and delete data. It can be configured with a single url like:

      url: "/api/todos/{id}"
      

      Or an object that configures how to create, retrieve, update and delete individually:

      url: {
        getListData: "GET /api/todos/find",
        getData: "GET /api/todo/get/{id}",
        createData: "POST /api/todo/create",
        updateData: "POST /api/todo/update?id={id}",
        destroyData: "POST /api/todo/delete?id={id}"
      }
      
    • ajax - Specify a method to use to make requests; can-ajax is used by default, but jQuery's .ajax method can be passed.

    • parseInstanceProp - Specify the property to find the data that represents an instance item.

    • parseInstanceData - Returns the properties that should be used to make an instance given the results of getData, createData, updateData, and destroyData.

    • parseListProp Specify the property to find the list data within a getList response.

    • parseListData Return the correctly formatted data for a getList response.

    • queryLogic - Specify the identity properties of the type. This is built automatically from the Map if can-define/map/map is used.

Returns

{connection}:

Returns a connection object.

restModel(url)

Create a connection with just a url. Use this if you do not need to pass in any other options to configure the connection.

For example, the following creates a Todo type with the ability to connect to a restful service layer:

import {todoFixture} from "//unpkg.com/can-demo-models@5";
import {restModel} from "can";

// Creates a mock backend with 5 todos
todoFixture(5);

const Todo = restModel("/api/todos/{id}").Map;

// Prints out all todo names
Todo.getList().then(todos => {
    todos.forEach(todo => {
        console.log(todo.name);
    })
})

Parameters

  1. url {String}:

    The url used to create, retrieve, update and delete data.

Returns

{connection}:

A connection that is the combination of the options and all the behaviors that restModel adds. The connection includes a Map property which is the type constructor function used to create instances of the raw record data retrieved from the server.

Use

Use restModel to build a simple connection to a restful service layer. To use restModel, you:

  • Define data types to connect to the service layer
  • Configure the connection to the service layer
  • Use the types to manipulate service data

restModel is the most basic built-in CanJS model layer. Check out can-realtime-rest-model for models that are able to:

  • Add and remove data from lists automatically
  • Unify instances across requests

Define data types

The first step in creating a model is to define the types that will be used to hold and manipulate data on the server. The following defines:

  • a Todo type to represent an individual todo's data
  • TodoList type to represent a list of todos
import {DefineMap, DefineList, restModel} from "can";

const Todo = DefineMap.extend("Todo",{
    id: {type: "number", identity: true},
    name: "string",
    complete: "boolean",
    createdAt: "date",
    toggle(){
        this.complete = !this.complete;
    }
})

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

Notice that properties and methods are defined on the types. While any of CanJS's map-types can be used to create a model, can-define/map/map currently is the easiest to configure.

Nested data type or data types with relationships

Sometimes your data might include nested data and/or related data. For example, if you get todo 5's data at /api/todos/5 and it returns a nested assignedTo as follows:

{
    id: 5,
    name: "mow lawn",
    complete: false,
    assignedTo: {
        id: 28,
        userName: "Justin Meyer"
    }
}

You typically want to define that nested value as another type like:

const User = DefineMap.extend("User",{
    id: "number",
    userName: "string"
});

const Todo = DefineMap.extend("Todo",{
    id: {type: "number", identity: true},
    name: "string",
    complete: "boolean",
    assignedTo: User,
    toggle(){
        this.complete = !this.complete;
    }
});

Check out the can/ref behavior for additional relationship features.

If you are using can-define/map/map and your server might add properties that can't be defined beforehand, make sure to unseal your todo type:

const Todo = DefineMap.extend("Todo",
{   
    seal: false
},
{
    id: {type: "number", identity: true},
    name: "string",
    complete: "boolean",
    toggle(){
        this.complete = !this.complete;
    }
});

Often with document-based data structures, it's nice to have a reference to the "root" data object on all child objects. For example, todo data might have a list of subtasks, each with their own name and complete status:

{
    id: 5,
    name: "mow lawn",
    complete: false,
    subtasks: [
        {name: "get gas", complete: true},
        {name: "sharpen blades", complete: false}
    ]
}

It can be nice to have the individual subtasks have a reference to their parent todo. For example, this makes updating the subtask easier. The following makes it so calling a subtask's .save() actually calls it's todo's .save() method:

import {DefineMap, DefineList, restModel} from "//unpkg.com/can@5/core.mjs";
import {todoFixture} from "//unpkg.com/can-demo-models@5";

// Model subtask
const Subtask = DefineMap.extend("Subtask",{
    name: "string",
    complete: "boolean",
    // parentTodo should not be serialized
    parentTodo: {serialize: false, type: "any"},
    // a save utility that actually saves the parent todo
    save(){
        this.parentTodo.save();
    }
});

// Model a list of subtasks to add the `parentTodo` to all subtasks
Subtask.List = DefineList.extend("Subtasks",{
    // Defines the items in the subtasks list
    "#": {
        Type: Subtask,
        // If subtasks are added, set their parentTodo
        added(subtasks){
            if(this.parentTodo) {
                subtasks.forEach((subtask) => {
                    subtask.parentTodo = this.parentTodo;
                })
            }
            return subtasks;
        },
        // If subtasks are removed, remove their parentTodo
        removed(subtasks) {
            subtasks.forEach((subtask) => {
                subtask.parentTodo = null;
            })
        }
    },
    // If parentTodo is set, update all the subtasks' parentTodo
    parentTodo: {
        set(parentTodo){
            this.forEach(function(subtask){
                subtask.parentTodo = parentTodo;
            });
            return parentTodo;
        },
        serialize: false
    }
});

const Todo = DefineMap.extend("Todo",{
    id: {type: "number", identity: true},
    name: "string",
    complete: "boolean",
    // Make it so when subtasks is set, it sets
    // the parentTodo reference:
    subtasks: {
        Type: Subtask.List,
        set(subtasks){
            subtasks.parentTodo = this;
            return subtasks;
        }
    },
    toggle(){
        this.complete = !this.complete;
    }
});

Todo.List = DefineList.extend("TodoList",{
    "#": Todo,
});

// Sets up a can-fixture as the backend
todoFixture(0);

// Creates a restModel
Todo.connection = restModel({
    Map: Todo,
    List: Todo.List,
    url: "/api/todos/{id}"
});

// Creates a new todo with one subtask
let myTodo = new Todo({
    name: "learn canjs", completed: false,
    subtasks: [{name: "learn js", completed: false}]
});

// Modifies and saves the subtask (thus saving the entire todo)
myTodo.subtasks[0].completed = true;
myTodo.subtasks[0].save();

// Reads the newly saved todo from the backend and prints it's completed status
Todo.getList().then(todos => console.log(todos[0].subtasks[0].completed));

The identity property

If you're specifying the identity property on nested data types, restModel will be able to intelligently merge data. For example, say a Todo and its nested User type are defined as follows:

const User = DefineMap.extend("User",{
    id: "number",
    name: "string"
});

const Todo = DefineMap.extend("Todo",{
    id: {type: "number", identity: true},
    name: "string",
    complete: "boolean",
    assignedTo: [User]
});

If a todo like the following:

let justin = new User({id: 20, name: "Justin"}),
    ramiya = new User({id: 21, name: "Ramiya"});

let todo = new Todo({
    id: 1,
    name: "mow lawn",
    complete: false,
    assignedTo: [justin, ramiya]
});

is updated with data like:

{
    id: 1,
    name: "mow lawn",
    complete: true,
    assignedTo: [{
        id: 21, name: "Ramiya Meyer"
    }]
}

Without specifying the identity property of User, the justin instance's id and name will be updated, not the ramiya instance's like you might expect:

justin.id //-> 21
justin.name //-> "Ramiya Meyer"

However, if the User object's id property is specified with an identity: true flag as follows:

const User = DefineMap.extend("User",{
    id: {type: "number", identity: true},
    name: "string"
});

When the update happens, the ramiya instance will be updated correctly:

ramiya.id //-> 21
ramiya.name //-> "Ramiya Meyer"

Configure the connection

Once you have your types defined, the next step is to configure your connection to make requests to your service layer and create these types.

If your service layer matches what CanJS expects, this configuration might be as simple as the following:

Todo.connection = restModel({
    Map: Todo,
    List: Todo.List,
    url: "/api/todos/{id}"
});

This configuration assumes the following:

  • GET /api/todos is used to retrieve a list of todos. It returns a JSON response like:

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

    Note that an object is returned with a data array. The array contains the data that will be used to create instances of the Todo type. Other properties on the object (ex: totalCount) will be added to the list type. The data above produces:

    todos instanceof Todo.List //-> true
    todos.totalCount          //-> 20
    todos[0] instanceof Todo  //-> true
    todos[0].id               //-> 5
    
  • GET /api/todos/5 is used to retrieve a single todo. It returns a JSON response like:

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

    Note that the object returned contains the values that will be used to create a Todo instance.

  • POST /api/todos is used to create a single todo record. It should take a JSON request body of the properties on a todo record like:

    { name: "do dishes", complete: false }
    

    The server should return a JSON response with the identity properties and any other values that should be included on the object:

    { id: 6, name: "do dishes", complete: false, createdAt: "2018-04-18" }
    
  • PUT /api/todos/6 is used to update a todo record. It should take a JSON request body of the properties of the todo record (with the exception of the identity keys) like:

    { name: "do dishes", complete: true, createdAt: "2018-04-18" }
    

    The server should return a JSON response with the full record:

    { id: 6, name: "do dishes", complete: true, createdAt: "2018-04-18" }
    
  • DELETE /api/todos/6 is used to delete a todo record. The server should return the record data:

    { id: 6, name: "do dishes", complete: true, createdAt: "2018-04-18" }
    

    or an empty successful response.

If your service layer doesn't match what CanJS expects, then you can configure either how the request is made or how the response is parsed.

The url option can be configured with individual urls used to create, retrieve, update and delete data:

Todo.connection = restModel({
    Map: Todo,
    List: Todo.List,
    url: {
        getListData: "GET /api/todos/find",
        getData: "GET /api/todo/get/{id}",
        createData: "POST /api/todo/create",
        updateData: "POST /api/todo/update?id={id}",
        destroyData: "POST /api/todo/delete?id={id}"
    }
});

You can also supply functions to retrieve the data yourself and return a promise that resolves to the expected data format. The following makes getListData use fetch to request JSON data:

import { param, restModel } from "can";

Todo.connection = restModel({
    Map: Todo,
    List: Todo.List,
    url: {
        getListData: function(query) {
            return fetch("/api/todos/find?"+param(query)).then(function(response){
                return response.json();
            })
        },
        getData: "GET /api/todo/get/{id}",
        createData: "POST /api/todo/create",
        updateData: "POST /api/todo/update?id={id}",
        destroyData: "POST /api/todo/delete?id={id}"
    }
});

If the response data doesn't match the expected format, you can either fix it in functions like getListData above or use parseInstanceProp, parseListProp, parseInstanceData or parseListData to fix the formatting. For example, if GET /api/todos returned data like:

{
    todos: [
        { id: 5, name: "mow lawn", complete: false },
        ...
    ],
    totalCount: 20
}

You could correct this with parseListProp like:

Todo.connection = restModel({
    Map: Todo,
    List: Todo.List,
    url: "/api/todos/{id}",
    parseListProp: "todos"
});

Manipulate service data

The below code allows one to retrieve, create, update, and destroy instances using methods on Todo and instances of Todo:

import {Todo, todoFixture} from "//unpkg.com/can-demo-models@5";
import {restModel} from "can";

// Creates a mock backend with 5 todos
todoFixture(5);

Todo.connection = restModel({
    Map: Todo,
    List: Todo.List,
    url: "/api/todos/{id}"
});

// get a list of todos
Todo.getList({filter: {complete: true}}) //-> Promise<Todo.List>

// get a single todo
Todo.get({id: 4}) //-> Promise<Todo>

// create a todo and persist it to the server:
let todo = new Todo({name: "learn canjs", complete: false})
todo.save() //-> Promise<Todo>

// update the todo and persist changes to the server:
todo.complete = true;
todo.save() //-> Promise<Todo>

// prints out all complete todos including the new one
Todo.getList({filter: {complete: true}})
  .then(todos => todos.forEach(todo => console.log(todo.name)))

// delete the todo on the server
todo.destroy() //-> Promise<Todo>

restModel also mixes in methods that let you know if the object is being saved, destroyed, or has already been created:

  • isSaving
  • isDestroying
  • isNew
todo.isSaving() //-> Boolean

todo.isDestroying() //-> Boolean

todo.isNew() //-> Boolean

These methods are observable, so they can be read in a template and the template will automatically update:

<button disabled:from="todo.isSaving()">Update</button>

restModel also makes the type and instances of the type emit events when items are created, updated or destroyed:

Todo.on("created", function(ev, newTodo) {
    console.log("Todo created event");
});

let todo = new Todo({name: "mow lawn"});
todo.on("created", function(){
    console.log("todo created event");
})

todo.save()
    //-> logs "todo created event"
    //   logs "Todo created event"

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

On this page

Get help

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