CanJS - Model

canjs

What is can.Model?

Model adds service encapsulation to can.Map. Model lets you get and modify data from the server, listen to changes by the server, keep track of all instances and prevent duplicates in the non-leaking store.

can.Model hooks up observables to your back end with declarative service bindings.

var Todo = can.Model({
  findAll : '/todo',
  findOne : '/todo/{id}',
  destroy : 'POST /todo/destroy/{id}',
  update  : 'POST /todo/{id}',
  create  : '/todo'
},{});

Todo.findOne({id: 5}, function( todo ) {
  todo.attr('name') 
});
Todo = can.Model.extend({
    findAll: 'GET /todos.json',
    findOne: 'GET /todos/{id}.json',
    create:  'POST /todos.json',
    update:  'PUT /todos/{id}.json',
    destroy: 'DELETE /todos/{id}.json' 
},{});

Models are special Observes that connect to RESTful services. They come with a set of methods designed to make it easy to manage changes remotely. To create a Model class, call can.Model and supply it with specific static properties that tell it how to interact with the server, along with any prototype properties or helper methods the Model may need.

When accessing a straightforward RESTful API, creating a Model class and an instance of that Model class might be as simple as this:

var Todo = can.Model({
    findAll: 'GET /todos',
    findOne: 'GET /todos/{id}',
    create: 'POST /todos',
    update: 'PUT /todos/{id}',
    destroy: 'DELETE /todos/{id}'
}, {});

var dishesTask = new Todo({description: 'Do the dishes.'});

By supplying the findAll, findOne, create, update, and destroy properties, you show a Model class how to communicate with the server. You can call findAll and findOne on the Model class to retrieve Models and save and destroy on Models to create, update, and delete them.

Why did CanJS choose to implement Models as Observes?

The fact that Models are implemented as Observes probably leads to other features such as live binding, and allow us to listen for changes in the models.

Because Models are Observes, don't forget to set all your properties using attr.

How can we invoke findAll?

can.Model.findAll retrieves a group of Models by making a call to a server. Here's how you call findAll on our Todo class above:

Todo.findAll({}, function(todos) {
    // todos is a can.Model.List of Todo Models.
}, function(xhr) {
    // handle errors
});

This will make a GET request to /todos, which should return JSON that looks similar to:

{
    "data": [
        {"id":1, "description":"Do the dishes."},
        {"id":2, "description":"Mow the lawn."},
        {"id":3, "description":"Finish the laundry."}
    ]
}

findAll will also accept an array from the service, but you probably should not be returning an array from a JSON service.

When the service has returned, findAll will massage the data into Model instances and put them in a can.Model.List (which is like a can.Observe.List for Models). The resulting List will be passed to the success callback in its second parameter. If there was an error, findAll will call the error callback in its third parameter and pass it the XmlHttpRequest object used to make the call.

findAll returns a can.Deferred that will resolve to a Model List of items if the call succeeds and rejects to the XmlHttpRequest object if there is an error.

How does findOne work?

can.Model.findOne works similarly to findAll, except that it retrieves a single Model from a service:

Todo.findOne({id: 1}, function(todo) {
    // todo is an instance of Todo
});

This makes a GET request to /todos/1, which should return JSON that looks similar to:

{
    "id":1,
    "description":"Do the dishes"
}

findOne returns a Deferred that resolves to the Todo if the call succeeds and rejects to the XmlHttpRequest object if there is an error.

How can we save a model instance?

You can call save on a Model instance to save it back to the server. If the Model has an id, it will be updated using the function specified under update. Otherwise, can.Model assumes the Model is new and creates the item on the server using the function in create.

Either way, the success callback in the first parameter will be called on a successful save with the updated Model; if an error occurs, the error callback in the second parameter will be called with the XmlHttpRequest object. Like findAll, save returns a Deferred that resolves to the updated Model on success and rejects to the XmlHttpRequest object on failure.

var shopping = new Todo({description: "Go grocery shopping."});

shopping.save(function(saved) {
    // saved is the saved Todo
    saved.attr('description', 'Remember the milk.');
    saved.save();
});

In the code above, the first time shopping.save() is called, can.Model will make a POST request to /todos with a request body of description=Go grocery shopping.. When the response comes back, it should have an id (say, 5) and that id property will be reflected in todo.

The second time saved.save() is called, saved has an id, so can.Model will make a PUT request to /todos/5 with a request body of description=Remember the milk..

How can we delete a model instance?

When you need to delete a Model's counterpart on the server, just call destroy on the Model, passing it success and error handlers just like save, except that the success handler will be passed the Model that has been deleted. destroy also retuns a Deferred, which resolves to the deleted Model and rejects to the XmlHttpRequest object.

var cats = new Todo({description: "Feed the cats."});

cats.save(function(saved) {
    saved.destroy(function(destroyed) {
        // destroyed is the Todo that was destroyed
    });
});

When destroy is called in the above code, can.Model makes a DELETE request to /todos/6.

What are the new events emitted by Models?

Because Models are Observes, you can bind to the same events as on any other Observe. In addition to those events, Models emit three new kinds of events:

  1. created, when an instance is created on the server.
  2. updated, when an instance is updated on the server.
  3. destroyed, when an instance is destroyed on the server.

For example, here is how you listen for an instance being created on the server:

var mop = new Todo({description: 'Mop the floor.'});
mop.bind('created', function(ev, created) {
    // created is the created Todo
});
mop.save();

You can also bind directly onto the Model class to listen for any time any instance is created, updated, or destroyed:

Todo.bind('created', function(ev, created) {
    // created is the created Todo
});

What is can.Model.List?

Model Lists (provided by can.Model.List) are Lists whose items are Models. When one of a Model List's elements are destroyed, that element is removed from the list.

Todo.findAll({}, function(todos) {
    todos.length; // 5
    todos[0].destroy(function() {
        todos.length; // 4
    }
});

How can we create a model instance and save it to the database?

Create a todo instance and call save(success, error) to create the todo on the server:

// create a todo instance
var todo = new Todo({name: "do the dishes"})

// save it on the server
todo.save();

How can we retrieve a list of model instances from the database?

Retrieve a list of todos from the server with findAll(params, success(items), error):

Todo.findAll({}, function( todos ){
    // print out the todo names
    $.each(todos, function(i, todo) {
        console.log( todo.name );
    });
});

How can we retrieve a single model instance from the database?

Retrieve a single todo from the server with findOne(params, success(item), error):

Todo.findOne({id: 5}, function( todo ){
    // print out the todo name
    console.log( todo.name );
});

How can we update a model instance and save it to the database?

Once an item has been created on the server, you can change its properties and call save to update it on the server:

// update the todos' name
todo.attr('name','Take out the trash')

// update it on the server
todo.save()

How can we delete a model instance from the database?

Call destroy(success, error) to delete an item on the server:

todo.destroy();

How can we listen for changes in the data?

Listening to changes in data is a critical part of the Model-View-Controller architecture. can.Model lets you listen to when an item is created, updated, destroyed or its properties are changed. Use Model.bind(eventType, handler(event, model)) to listen to all events of type on a model and model.bind(eventType, handler(event)) to listen to events on a specific instance.

// listen for when any todo is created
Todo.bind('created', function( ev, todo ) {...})

// listen for when a specific todo is created
var todo = new Todo({name: 'do dishes'})
todo.bind('created', function( ev ) {...})

// listen for when any todo is updated
Todo.bind('updated', function( ev, todo ) {...})

// listen for when a specific todo is created
Todo.findOne({id: 6}, function( todo ) {
    todo.bind('updated', function( ev ) {...})
})

// listen for when any todo is destroyed
Todo.bind('destroyed', function( ev, todo ) {...})

// listen for when a specific todo is destroyed
todo.bind('destroyed', function( ev ) {...})

// listen for when the name property changes
todo.bind('name', function(ev){  })

How can we listen for model events using can.Control or can.Component?

You can use can.Control or the events property of can.Component to listen to model changes like:

Todos = can.Control.extend({
    "{Todo} updated" : function(Todo, ev, todo) {...}
})
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License