todomvc
Version:
> Helping you select an MV\* framework
1,558 lines (1,514 loc) • 51.7 kB
JavaScript
/*!
* CanJS - 2.0.3
* http://canjs.us/
* Copyright (c) 2013 Bitovi
* Tue, 26 Nov 2013 18:21:22 GMT
* Licensed MIT
* Includes: CanJS default build
* Download from: http://canjs.us/
*/
define(["can/util/library", "can/map", "can/list"], function( can ) {
// ## model.js
// `can.Model`
// _A `can.Map` that connects to a RESTful interface._
//
// Generic deferred piping function
/**
* @add can.Model
*/
var pipe = function( def, model, func ) {
var d = new can.Deferred();
def.then(function(){
var args = can.makeArray( arguments ),
success = true;
try {
args[0] = model[func](args[0]);
} catch(e) {
success = false;
d.rejectWith(d, [e].concat(args));
}
if (success) {
d.resolveWith(d, args);
}
},function(){
d.rejectWith(this, arguments);
});
if(typeof def.abort === 'function') {
d.abort = function() {
return def.abort();
}
}
return d;
},
modelNum = 0,
ignoreHookup = /change.observe\d+/,
getId = function( inst ) {
// Instead of using attr, use __get for performance.
// Need to set reading
can.__reading && can.__reading(inst, inst.constructor.id)
return inst.__get(inst.constructor.id);
},
// Ajax `options` generator function
ajax = function( ajaxOb, data, type, dataType, success, error ) {
var params = {};
// If we get a string, handle it.
if ( typeof ajaxOb == "string" ) {
// If there's a space, it's probably the type.
var parts = ajaxOb.split(/\s+/);
params.url = parts.pop();
if ( parts.length ) {
params.type = parts.pop();
}
} else {
can.extend( params, ajaxOb );
}
// If we are a non-array object, copy to a new attrs.
params.data = typeof data == "object" && ! can.isArray( data ) ?
can.extend(params.data || {}, data) : data;
// Get the url with any templated values filled out.
params.url = can.sub(params.url, params.data, true);
return can.ajax( can.extend({
type: type || "post",
dataType: dataType ||"json",
success : success,
error: error
}, params ));
},
makeRequest = function( self, type, success, error, method ) {
var args;
// if we pass an array as `self` it it means we are coming from
// the queued request, and we're passing already serialized data
// self's signature will be: [self, serializedData]
if(can.isArray(self)){
args = self[1];
self = self[0];
} else {
args = self.serialize();
}
args = [args];
var deferred,
// The model.
model = self.constructor,
jqXHR;
// `update` and `destroy` need the `id`.
if ( type !== 'create' ) {
args.unshift(getId(self));
}
jqXHR = model[type].apply(model, args);
deferred = jqXHR.pipe(function(data){
self[method || type + "d"](data, jqXHR);
return self;
});
// Hook up `abort`
if(jqXHR.abort){
deferred.abort = function(){
jqXHR.abort();
};
}
deferred.then(success,error);
return deferred;
},
initializers = {
// makes a models function that looks up the data in a particular property
models: function(prop){
return function( instancesRawData, oldList ) {
// until "end of turn", increment reqs counter so instances will be added to the store
can.Model._reqs++;
if ( ! instancesRawData ) {
return;
}
if ( instancesRawData instanceof this.List ) {
return instancesRawData;
}
// Get the list type.
var self = this,
tmp = [],
res = oldList instanceof can.List ? oldList : new( self.List || ML),
// Did we get an `array`?
arr = can.isArray(instancesRawData),
// Did we get a model list?
ml = (instancesRawData instanceof ML),
// Get the raw `array` of objects.
raw = arr ?
// If an `array`, return the `array`.
instancesRawData :
// Otherwise if a model list.
(ml ?
// Get the raw objects from the list.
instancesRawData.serialize() :
// Get the object's data.
can.getObject( prop||"data", instancesRawData)),
i = 0;
if(typeof raw === 'undefined') {
throw new Error('Could not get any raw data while converting using .models');
}
if(res.length) {
res.splice(0);
}
can.each(raw, function( rawPart ) {
tmp.push( self.model( rawPart ));
});
// We only want one change event so push everything at once
res.push.apply(res, tmp);
if ( ! arr ) { // Push other stuff onto `array`.
can.each(instancesRawData, function(val, prop){
if ( prop !== 'data' ) {
res.attr(prop, val);
}
})
}
// at "end of turn", clean up the store
setTimeout(can.proxy(this._clean, this), 1);
return res;
}
},
model: function( prop ) {
return function( attributes ) {
if ( ! attributes ) {
return;
}
if ( typeof attributes.serialize === 'function' ) {
attributes = attributes.serialize();
}
if(prop){
attributes = can.getObject( prop||"data", attributes );
}
var id = attributes[ this.id ],
model = (id || id === 0) && this.store[id] ?
this.store[id].attr(attributes, this.removeAttr || false) : new this( attributes );
return model;
}
}
}
// This object describes how to make an ajax request for each ajax method.
// The available properties are:
// `url` - The default url to use as indicated as a property on the model.
// `type` - The default http request type
// `data` - A method that takes the `arguments` and returns `data` used for ajax.
/**
* @static
*/
//
/**
* @function can.Model.bind bind
* @parent can.Model.static
* @description Listen for events on a Model class.
*
* @signature `can.Model.bind(eventType, handler)`
* @param {String} eventType The type of event. It must be
* `"created"`, `"updated"`, `"destroyed"`.
* @param {function} handler A callback function
* that gets called with the event and instance that was
* created, destroyed, or updated.
* @return {can.Model} The model constructor function.
*
* @body
* `bind(eventType, handler(event, instance))` listens to
* __created__, __updated__, __destroyed__ events on all
* instances of the model.
*
* Task.bind("created", function(ev, createdTask){
* this //-> Task
* createdTask.attr("name") //-> "Dishes"
* })
*
* new Task({name: "Dishes"}).save();
*/
//
/**
* @function can.Model.unbind unbind
* @parent can.Model.static
* @description Stop listening for events on a Model class.
*
* @signature `can.Model.unbind(eventType, handler)`
* @param {String} eventType The type of event. It must be
* `"created"`, `"updated"`, `"destroyed"`.
* @param {function} handler A callback function
* that was passed to `bind`.
* @return {can.Model} The model constructor function.
*
* @body
* `unbind(eventType, handler)` removes a listener
* attached with [can.Model.bind].
*
* var handler = function(ev, createdTask){
*
* }
* Task.bind("created", handler)
* Task.unbind("created", handler)
*
* You have to pass the same function to `unbind` that you
* passed to `bind`.
*/
//
/**
* @property {String} can.Model.id id
* @parent can.Model.static
* The name of the id field. Defaults to `'id'`. Change this if it is something different.
*
* For example, it's common in .NET to use `'Id'`. Your model might look like:
*
* Friend = can.Model.extend({
* id: "Id"
* },{});
*/
/**
* @property {Boolean} can.Model.removeAttr removeAttr
* @parent can.Model.static
* Sets whether model conversion should remove non existing attributes or merge with
* the existing attributes. The default is `false`.
* For example, if `Task.findOne({ id: 1 })` returns
*
* { id: 1, name: 'Do dishes', index: 1, color: ['red', 'blue'] }
*
* for the first request and
*
* { id: 1, name: 'Really do dishes', color: ['green'] }
*
* for the next request, the actual model attributes would look like:
*
* { id: 1, name: 'Really do dishes', index: 1, color: ['green', 'blue'] }
*
* Because the attributes of the original model and the updated model will
* be merged. Setting `removeAttr` to `true` will result in model attributes like
*
* { id: 1, name: 'Really do dishes', color: ['green'] }
*
*/
ajaxMethods = {
/**
* @description Specifies how to create a new resource on the server. `create(serialized)` is called
* by [can.Model.prototype.save save] if the model instance [can.Model.prototype.isNew is new].
* @function can.Model.create create
* @parent can.Model.static
*
*
* @signature `can.Model.create: function(serialized) -> deferred`
*
* Specify a function to create persistent instances. The function will
* typically perform an AJAX request to a service that results in
* creating a record in a database.
*
* @param {Object} serialized The [can.Map::serialize serialized] properties of
* the model to create.
* @return {can.Deferred} A Deferred that resolves to an object of attributes
* that will be added to the created model instance. The object __MUST__ contain
* an [can.Model.id id] property so that future calls to [can.Model.prototype.save save]
* will call [can.Model.update].
*
*
* @signature `can.Model.create: "[METHOD] /path/to/resource"`
*
* Specify a HTTP method and url to create persistent instances.
*
* If you provide a URL, the Model will send a request to that URL using
* the method specified (or POST if none is specified) when saving a
* new instance on the server. (See below for more details.)
*
* @param {HttpMethod} METHOD An HTTP method. Defaults to `"POST"`.
* @param {STRING} url The URL of the service to retrieve JSON data.
*
*
* @signature `can.Model.create: {ajaxSettings}`
*
* Specify an options object that is used to make a HTTP request to create
* persistent instances.
*
* @param {can.AjaxSettings} ajaxSettings A settings object that
* specifies the options available to pass to [can.ajax].
*
* @body
*
* `create(attributes) -> Deferred` is used by [can.Model::save save] to create a
* model instance on the server.
*
* ## Implement with a URL
*
* The easiest way to implement create is to give it the url
* to post data to:
*
* var Recipe = can.Model.extend({
* create: "/recipes"
* },{})
*
* This lets you create a recipe like:
*
* new Recipe({name: "hot dog"}).save();
*
*
* ## Implement with a Function
*
* You can also implement create by yourself. Create gets called
* with `attrs`, which are the [can.Map::serialize serialized] model
* attributes. Create returns a `Deferred`
* that contains the id of the new instance and any other
* properties that should be set on the instance.
*
* For example, the following code makes a request
* to `POST /recipes.json {'name': 'hot+dog'}` and gets back
* something that looks like:
*
* {
* "id": 5,
* "createdAt": 2234234329
* }
*
* The code looks like:
*
* can.Model.extend("Recipe", {
* create : function( attrs ){
* return $.post("/recipes.json",attrs, undefined ,"json");
* }
* },{})
*/
create : {
url : "_shortName",
type :"post"
},
/**
* @description Update a resource on the server.
* @function can.Model.update update
* @parent can.Model.static
* @signature `can.Model.update: "[METHOD] /path/to/resource"`
* If you provide a URL, the Model will send a request to that URL using
* the method specified (or PUT if none is specified) when updating an
* instance on the server. (See below for more details.)
* @return {can.Deferred} A Deferred that resolves to the updated model.
*
* @signature `can.Model.update: function(id, serialized) -> can.Deffered`
* If you provide a function, the Model will expect you to do your own AJAX requests.
* @param {*} id The ID of the model to update.
* @param {Object} serialized The [can.Map::serialize serialized] properties of
* the model to update.
* @return {can.Deferred} A Deferred that resolves to the updated model.
*
* @body
* `update( id, attrs ) -> Deferred` is used by [can.Model::save save] to
* update a model instance on the server.
*
* ## Implement with a URL
*
* The easist way to implement update is to just give it the url to `PUT` data to:
*
* Recipe = can.Model.extend({
* update: "/recipes/{id}"
* },{});
*
* This lets you update a recipe like:
*
* Recipe.findOne({id: 1}, function(recipe){
* recipe.attr('name','salad');
* recipe.save();
* })
*
* This will make an XHR request like:
*
* PUT /recipes/1
* name=salad
*
* If your server doesn't use PUT, you can change it to post like:
*
* Recipe = can.Model.extend({
* update: "POST /recipes/{id}"
* },{});
*
* The server should send back an object with any new attributes the model
* should have. For example if your server updates the "updatedAt" property, it
* should send back something like:
*
* // PUT /recipes/4 {name: "Food"} ->
* {
* updatedAt : "10-20-2011"
* }
*
* ## Implement with a Function
*
* You can also implement update by yourself. Update takes the `id` and
* `attributes` of the instance to be updated. Update must return
* a [can.Deferred Deferred] that resolves to an object that contains any
* properties that should be set on the instance.
*
* For example, the following code makes a request
* to '/recipes/5.json?name=hot+dog' and gets back
* something that looks like:
*
* {
* updatedAt: "10-20-2011"
* }
*
* The code looks like:
*
* Recipe = can.Model.extend({
* update : function(id, attrs ) {
* return $.post("/recipes/"+id+".json",attrs, null,"json");
* }
* },{});
*/
update : {
data : function(id, attrs){
attrs = attrs || {};
var identity = this.id;
if ( attrs[identity] && attrs[identity] !== id ) {
attrs["new" + can.capitalize(id)] = attrs[identity];
delete attrs[identity];
}
attrs[identity] = id;
return attrs;
},
type : "put"
},
/**
* @description Destroy a resource on the server.
* @function can.Model.destroy destroy
* @parent can.Model.static
*
* @signature `can.Model.destroy: function(id) -> deferred`
*
*
*
* If you provide a function, the Model will expect you to do your own AJAX requests.
* @param {*} id The ID of the resource to destroy.
* @return {can.Deferred} A Deferred that resolves to the destroyed model.
*
*
* @signature `can.Model.destroy: "[METHOD] /path/to/resource"`
*
* If you provide a URL, the Model will send a request to that URL using
* the method specified (or DELETE if none is specified) when deleting an
* instance on the server. (See below for more details.)
*
* @return {can.Deferred} A Deferred that resolves to the destroyed model.
*
*
*
* @body
* `destroy(id) -> Deferred` is used by [can.Model::destroy] remove a model
* instance from the server.
*
* ## Implement with a URL
*
* You can implement destroy with a string like:
*
* Recipe = can.Model.extend({
* destroy : "/recipe/{id}"
* },{})
*
* And use [can.Model::destroy] to destroy it like:
*
* Recipe.findOne({id: 1}, function(recipe){
* recipe.destroy();
* });
*
* This sends a `DELETE` request to `/thing/destroy/1`.
*
* If your server does not support `DELETE` you can override it like:
*
* Recipe = can.Model.extend({
* destroy : "POST /recipe/destroy/{id}"
* },{})
*
* ## Implement with a function
*
* Implement destroy with a function like:
*
* Recipe = can.Model.extend({
* destroy : function(id){
* return $.post("/recipe/destroy/"+id,{});
* }
* },{})
*
* Destroy just needs to return a deferred that resolves.
*/
destroy : {
type : "delete",
data : function(id, attrs){
attrs = attrs || {};
attrs.id = attrs[this.id] = id;
return attrs;
}
},
/**
* @description Retrieve multiple resources from a server.
* @function can.Model.findAll findAll
* @parent can.Model.static
*
* @signature `can.Model.findAll( params[, success[, error]] )`
*
* Retrieve multiple resources from a server.
*
* @param {Object} params Values to filter the request or results with.
* @param {function(can.Model.List)} [success(list)] A callback to call on successful retrieval. The callback recieves
* a can.Model.List of the retrieved resources.
* @param {function(can.AjaxSettings)} [error(xhr)] A callback to call when an error occurs. The callback receives the
* XmlHttpRequest object.
* @return {can.Deferred} A deferred that resolves to a [can.Model.List] of retrieved models.
*
*
* @signature `can.Model.findAll: findAllData( params ) -> deferred`
*
* Implements `findAll` with a [can.Model.findAllData function]. This function
* is passed to [can.Model.makeFindAll makeFindAll] to create the external
* `findAll` method.
*
* findAll: function(params){
* return $.get("/tasks",params)
* }
*
* @param {can.Model.findAllData} findAllData A function that accepts parameters
* specifying a list of instance data to retrieve and returns a [can.Deferred]
* that resolves to an array of those instances.
*
* @signature `can.Model.findAll: "[METHOD] /path/to/resource"`
*
* Implements `findAll` with a HTTP method and url to retrieve instance data.
*
* findAll: "GET /tasks"
*
* If `findAll` is implemented with a string, this gets converted to
* a [can.Model.findAllData findAllData function]
* which is passed to [can.Model.makeFindAll makeFindAll] to create the external
* `findAll` method.
*
* @param {HttpMethod} METHOD An HTTP method. Defaults to `"GET"`.
*
* @param {STRING} url The URL of the service to retrieve JSON data.
*
* @return {JSON} The service should return a JSON object like:
*
* {
* "data": [
* { "id" : 1, "name" : "do the dishes" },
* { "id" : 2, "name" : "mow the lawn" },
* { "id" : 3, "name" : "iron my shirts" }
* ]
* }
*
* This object is passed to [can.Model.models] to turn it into instances.
*
* _Note: .findAll can also accept an array, but you
* probably [should not be doing that](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx)._
*
*
* @signature `can.Model.findAll: {ajaxSettings}`
*
* Implements `findAll` with a [can.AjaxSettings ajax settings object].
*
* findAll: {url: "/tasks", dataType: "json"}
*
* If `findAll` is implemented with an object, it gets converted to
* a [can.Model.findAllData findAllData function]
* which is passed to [can.Model.makeFindAll makeFindAll] to create the external
* `findAll` method.
*
* @param {can.AjaxSettings} ajaxSettings A settings object that
* specifies the options available to pass to [can.ajax].
*
* @body
*
* ## Use
*
* `findAll( params, success(instances), error(xhr) ) -> Deferred` is used to retrieve model
* instances from the server. After implementing `findAll`, use it to retrieve instances of the model
* like:
*
* Recipe.findAll({favorite: true}, function(recipes){
* recipes[0].attr('name') //-> "Ice Water"
* }, function( xhr ){
* // called if an error
* }) //-> Deferred
*
*
* Before you can use `findAll`, you must implement it.
*
* ## Implement with a URL
*
* Implement findAll with a url like:
*
* Recipe = can.Model.extend({
* findAll : "/recipes.json"
* },{});
*
* The server should return data that looks like:
*
* [
* {"id" : 57, "name": "Ice Water"},
* {"id" : 58, "name": "Toast"}
* ]
*
* ## Implement with an Object
*
* Implement findAll with an object that specifies the parameters to
* `can.ajax` (jQuery.ajax) like:
*
* Recipe = can.Model.extend({
* findAll : {
* url: "/recipes.xml",
* dataType: "xml"
* }
* },{})
*
* ## Implement with a Function
*
* To implement with a function, `findAll` is passed __params__ to filter
* the instances retrieved from the server and it should return a
* deferred that resolves to an array of model data. For example:
*
* Recipe = can.Model.extend({
* findAll : function(params){
* return $.ajax({
* url: '/recipes.json',
* type: 'get',
* dataType: 'json'})
* }
* },{})
*
*/
findAll : {
url : "_shortName"
},
/**
* @description Retrieve a resource from a server.
* @function can.Model.findOne findOne
* @parent can.Model.static
*
* @signature `can.Model.findOne( params[, success[, error]] )`
*
* Retrieve a single instance from the server.
*
* @param {Object} params Values to filter the request or results with.
* @param {function(can.Model)} [success(model)] A callback to call on successful retrieval. The callback recieves
* the retrieved resource as a can.Model.
* @param {function(can.AjaxSettings)} [error(xhr)] A callback to call when an error occurs. The callback receives the
* XmlHttpRequest object.
* @return {can.Deferred} A deferred that resolves to a [can.Model.List] of retrieved models.
*
* @signature `can.Model.findOne: findOneData( params ) -> deferred`
*
* Implements `findOne` with a [can.Model.findOneData function]. This function
* is passed to [can.Model.makeFindOne makeFindOne] to create the external
* `findOne` method.
*
* findOne: function(params){
* return $.get("/task/"+params.id)
* }
*
* @param {can.Model.findOneData} findOneData A function that accepts parameters
* specifying an instance to retreive and returns a [can.Deferred]
* that resolves to that instance.
*
* @signature `can.Model.findOne: "[METHOD] /path/to/resource"`
*
* Implements `findOne` with a HTTP method and url to retrieve an instance's data.
*
* findOne: "GET /tasks/{id}"
*
* If `findOne` is implemented with a string, this gets converted to
* a [can.Model.makeFindOne makeFindOne function]
* which is passed to [can.Model.makeFindOne makeFindOne] to create the external
* `findOne` method.
*
* @param {HttpMethod} METHOD An HTTP method. Defaults to `"GET"`.
*
* @param {STRING} url The URL of the service to retrieve JSON data.
*
* @signature `can.Model.findOne: {ajaxSettings}`
*
* Implements `findOne` with a [can.AjaxSettings ajax settings object].
*
* findOne: {url: "/tasks/{id}", dataType: "json"}
*
* If `findOne` is implemented with an object, it gets converted to
* a [can.Model.makeFindOne makeFindOne function]
* which is passed to [can.Model.makeFindOne makeFindOne] to create the external
* `findOne` method.
*
* @param {can.AjaxSettings} ajaxSettings A settings object that
* specifies the options available to pass to [can.ajax].
*
* @body
*
* ## Use
*
* `findOne( params, success(instance), error(xhr) ) -> Deferred` is used to retrieve a model
* instance from the server.
*
* Use `findOne` like:
*
* Recipe.findOne({id: 57}, function(recipe){
* recipe.attr('name') //-> "Ice Water"
* }, function( xhr ){
* // called if an error
* }) //-> Deferred
*
* Before you can use `findOne`, you must implement it.
*
* ## Implement with a URL
*
* Implement findAll with a url like:
*
* Recipe = can.Model.extend({
* findOne : "/recipes/{id}.json"
* },{});
*
* If `findOne` is called like:
*
* Recipe.findOne({id: 57});
*
* The server should return data that looks like:
*
* {"id" : 57, "name": "Ice Water"}
*
* ## Implement with an Object
*
* Implement `findOne` with an object that specifies the parameters to
* `can.ajax` (jQuery.ajax) like:
*
* Recipe = can.Model.extend({
* findOne : {
* url: "/recipes/{id}.xml",
* dataType: "xml"
* }
* },{})
*
* ## Implement with a Function
*
* To implement with a function, `findOne` is passed __params__ to specify
* the instance retrieved from the server and it should return a
* deferred that resolves to the model data. Also notice that you now need to
* build the URL manually. For example:
*
* Recipe = can.Model.extend({
* findOne : function(params){
* return $.ajax({
* url: '/recipes/' + params.id,
* type: 'get',
* dataType: 'json'})
* }
* },{})
*
*
*/
findOne: {}
},
// Makes an ajax request `function` from a string.
// `ajaxMethod` - The `ajaxMethod` object defined above.
// `str` - The string the user provided. Ex: `findAll: "/recipes.json"`.
ajaxMaker = function(ajaxMethod, str){
// Return a `function` that serves as the ajax method.
return function(data){
// If the ajax method has it's own way of getting `data`, use that.
data = ajaxMethod.data ?
ajaxMethod.data.apply(this, arguments) :
// Otherwise use the data passed in.
data;
// Return the ajax method with `data` and the `type` provided.
return ajax(str || this[ajaxMethod.url || "_url"], data, ajaxMethod.type || "get")
}
}
can.Model = can.Map({
fullName: "can.Model",
_reqs: 0,
/**
* @hide
* @function can.Model.setup
* @parent can.Model.static
*
* Configures
*
*/
setup : function(base){
// create store here if someone wants to use model without inheriting from it
this.store = {};
can.Map.setup.apply(this, arguments);
// Set default list as model list
if(!can.Model){
return;
}
/**
* @property {can.Model.List} can.Model.static.List List
* @parent can.Model.static
*
* @description Specifies the type of List that [can.Model.findAll findAll]
* should return.
*
* @option {can.Model.List} A can.Model's List property is the
* type of [can.List List] returned
* from [can.Model.findAll findAll]. For example:
*
* Task = can.Model.extend({
* findAll: "/tasks"
* },{})
*
* Task.findAll({}, function(tasks){
* tasks instanceof Task.List //-> true
* })
*
* Overwrite a Model's `List` property to add custom
* behavior to the lists provided to `findAll` like:
*
* Task = can.Model.extend({
* findAll: "/tasks"
* },{})
* Task.List = Task.List.extend({
* completed: function(){
* var count = 0;
* this.each(function(task){
* if( task.attr("completed") ) count++;
* })
* return count;
* }
* })
*
* Task.findAll({}, function(tasks){
* tasks.completed() //-> 3
* })
*
* When [can.Model] is extended,
* [can.Model.List] is extended and set as the extended Model's
* `List` property. The extended list's [can.List.Map Map] property
* is set to the extended Model. For example:
*
* Task = can.Model.extend({
* findAll: "/tasks"
* },{})
* Task.List.Map //-> Task
*
*/
this.List = ML({Map: this},{});
var self = this,
clean = can.proxy(this._clean, self);
// go through ajax methods and set them up
can.each(ajaxMethods, function(method, name){
// if an ajax method is not a function, it's either
// a string url like findAll: "/recipes" or an
// ajax options object like {url: "/recipes"}
if ( ! can.isFunction( self[name] )) {
// use ajaxMaker to convert that into a function
// that returns a deferred with the data
self[name] = ajaxMaker(method, self[name]);
}
// check if there's a make function like makeFindAll
// these take deferred function and can do special
// behavior with it (like look up data in a store)
if (self["make"+can.capitalize(name)]){
// pass the deferred method to the make method to get back
// the "findAll" method.
var newMethod = self["make"+can.capitalize(name)](self[name]);
can.Construct._overwrite(self, base, name,function(){
// increment the numer of requests
can.Model._reqs++;
var def = newMethod.apply(this, arguments);
var then = def.then(clean, clean);
then.abort = def.abort;
// attach abort to our then and return it
return then;
})
}
});
can.each(initializers, function(makeInitializer, name){
if( typeof self[name] === "string" ) {
can.Construct._overwrite( self, base, name, makeInitializer( self[name] ) )
}
})
if(self.fullName == "can.Model" || !self.fullName){
self.fullName = "Model"+(++modelNum);
}
// Add ajax converters.
can.Model._reqs = 0;
this._url = this._shortName+"/{"+this.id+"}"
},
_ajax : ajaxMaker,
_makeRequest : makeRequest,
_clean : function(){
can.Model._reqs--;
if(!can.Model._reqs){
for(var id in this.store) {
if(!this.store[id]._bindings){
delete this.store[id];
}
}
}
return arguments[0];
},
/**
* @function can.Model.models models
* @parent can.Model.static
* @description Convert raw data into can.Model instances.
*
* @signature `can.Model.models(data[, oldList])`
* @param {Array<Object>} data The raw data from a `[can.Model.findAll findAll()]` request.
* @param {can.Model.List} [oldList] If supplied, this List will be updated with the data from
* __data__.
* @return {can.Model.List} A List of Models made from the raw data.
*
* @signature `models: "PROPERTY"`
*
* Creates a `models` function that looks for the array of instance data in the PROPERTY
* property of the raw response data of [can.Model.findAll].
*
* @body
* `can.Model.models(data, xhr)` is used to
* convert the raw response of a [can.Model.findAll] request
* into a [can.Model.List] of model instances.
*
* This method is rarely called directly. Instead the deferred returned
* by findAll is piped into `models`. This creates a new deferred that
* resolves to a [can.Model.List] of instances instead of an array of
* simple JS objects.
*
* If your server is returning data in non-standard way,
* overwriting `can.Model.models` is the best way to normalize it.
*
* ## Quick Example
*
* The following uses models to convert to a [can.Model.List] of model
* instances.
*
* Task = can.Model.extend()
* var tasks = Task.models([
* {id: 1, name : "dishes", complete : false},
* {id: 2, name: "laundry", compelte: true}
* ])
*
* tasks.attr("0.complete", true)
*
* ## Non-standard Services
*
* `can.Model.models` expects data to be an array of name-value pair
* objects like:
*
* [{id: 1, name : "dishes"},{id:2, name: "laundry"}, ...]
*
* It can also take an object with additional data about the array like:
*
* {
* count: 15000 //how many total items there might be
* data: [{id: 1, name : "justin"},{id:2, name: "brian"}, ...]
* }
*
* In this case, models will return a [can.Model.List] of instances found in
* data, but with additional properties as expandos on the list:
*
* var tasks = Task.models({
* count : 1500,
* data : [{id: 1, name: 'dishes'}, ...]
* })
* tasks.attr("name") // -> 'dishes'
* tasks.count // -> 1500
*
* ### Overwriting Models
*
* If your service returns data like:
*
* {thingsToDo: [{name: "dishes", id: 5}]}
*
* You will want to overwrite models to pass the base models what it expects like:
*
* Task = can.Model.extend({
* models : function(data){
* return can.Model.models.call(this,data.thingsToDo);
* }
* },{})
*
* `can.Model.models` passes each instance's data to `can.Model.model` to
* create the individual instances.
*/
models: initializers.models("data"),
/**
* @function can.Model.model model
* @parent can.Model.static
* @description Convert raw data into a can.Model instance.
* @signature `can.Model.model(data)`
* @param {Object} data The data to convert to a can.Model instance.
* @return {can.Model} An instance of can.Model made with the given data.
*
* @signature `model: "PROPERTY"`
*
* Creates a `model` function that looks for the attributes object in the PROPERTY
* property of raw instance data.
*
* @body
* `can.Model.model(attributes)` is used to convert data from the server into
* a model instance. It is rarely called directly. Instead it is invoked as
* a result of [can.Model.findOne] or [can.Model.findAll].
*
* If your server is returning data in non-standard way,
* overwriting `can.Model.model` is a good way to normalize it.
*
* ## Example
*
* The following uses `model` to convert to a model
* instance.
*
* Task = can.Model.extend({},{})
* var task = Task.model({id: 1, name : "dishes", complete : false})
*
* tasks.attr("complete", true)
*
* `Task.model(attrs)` is very similar to simply calling `new Model(attrs)` except
* that it checks the model's store if the instance has already been created. The model's
* store is a collection of instances that have event handlers.
*
* This means that if the model's store already has an instance, you'll get the same instance
* back. Example:
*
* // create a task
* var taskA = new Task({id: 5, complete: true});
*
* // bind to it, which puts it in the store
* taskA.bind("complete", function(){});
*
* // use model to create / retrieve a task
* var taskB = Task.model({id: 5, complete: true});
*
* taskA === taskB //-> true
*
* ## Non-standard Services
*
* `can.Model.model` expects to retreive attributes of the model
* instance like:
*
*
* {id: 5, name : "dishes"}
*
*
* If the service returns data formatted differently, like:
*
* {todo: {name: "dishes", id: 5}}
*
* Overwrite `model` like:
*
* Task = can.Model.extend({
* model : function(data){
* return can.Model.model.call(this,data.todo);
* }
* },{});
*/
model: initializers.model()
},
/**
* @prototype
*/
{
setup: function(attrs){
// try to add things as early as possible to the store (#457)
// we add things to the store before any properties are even set
var id = attrs && attrs[this.constructor.id];
if(can.Model._reqs && id != null ){
this.constructor.store[id] = this;
}
can.Map.prototype.setup.apply(this, arguments)
},
/**
* @function can.Model.prototype.isNew isNew
* @description Check if a Model has yet to be saved on the server.
* @signature `model.isNew()`
* @return {Boolean} Whether an instance has been saved on the server.
* (This is determined by whether `id` has a value set yet.)
*
* @body
* `isNew()` returns if the instance is has been created
* on the server. This is essentially if the [can.Model.id]
* property is null or undefined.
*
* new Recipe({id: 1}).isNew() //-> false
*/
isNew: function() {
var id = getId(this);
return ! ( id || id === 0 ); // If `null` or `undefined`
},
/**
* @function can.Model.prototype.save save
* @description Save a model back to the server.
* @signature `model.save([success[, error]])`
* @param {function} [success] A callback to call on successful save. The callback recieves
* the can.Model after saving.
* @param {function} [error] A callback to call when an error occurs. The callback receives the
* XmlHttpRequest object.
* @return {can.Deferred} A Deferred that resolves to the Model after it has been saved.
*
* @body
* `model.save([success(model)],[error(xhr)])` creates or updates
* the model instance using [can.Model.create] or
* [can.Model.update] depending if the instance
* [can.Model::isNew has an id or not].
*
* ## Using `save` to create an instance.
*
* If `save` is called on an instance that does not have
* an [can.Model.id id] property, it calls [can.Model.create]
* with the instance's properties. It also [can.trigger triggers]
* a "created" event on the instance and the model.
*
* // create a model instance
* var todo = new Todo({name: "dishes"})
*
* // listen when the instance is created
* todo.bind("created", function(ev){
* this //-> todo
* })
*
* // save it on the server
* todo.save(function(todo){
* console.log("todo", todo, "created")
* });
*
* ## Using `save` to update an instance.
*
* If save is called on an instance that has
* an [can.Model.id id] property, it calls [can.Model.create]
* with the instance's properties. When the save is complete,
* it triggers an "updated" event on the instance and the instance's model.
*
* Instances with an
* __id__ are typically retrieved with [can.Model.findAll] or
* [can.Model.findOne].
*
*
* // get a created model instance
* Todo.findOne({id: 5},function(todo){
*
* // listen when the instance is updated
* todo.bind("updated", function(ev){
* this //-> todo
* })
*
* // update the instance's property
* todo.attr("complete", true)
*
* // save it on the server
* todo.save(function(todo){
* console.log("todo", todo, "updated")
* });
*
* });
*
*/
save: function( success, error ) {
return makeRequest(this, this.isNew() ? 'create' : 'update', success, error);
},
/**
* @function can.Model.prototype.destroy destroy
* @description Destroy a Model on the server.
* @signature `model.destroy([success[, error]])`
* @param {function} [success] A callback to call on successful destruction. The callback recieves
* the can.Model as it was just prior to destruction.
* @param {function} [error] A callback to call when an error occurs. The callback receives the
* XmlHttpRequest object.
* @return {can.Deferred} A Deferred that resolves to the Model as it was before destruction.
*
* @body
* Destroys the instance by calling
* [Can.Model.destroy] with the id of the instance.
*
* recipe.destroy(success, error);
*
* This triggers "destroyed" events on the instance and the
* Model constructor function which can be listened to with
* [can.Model::bind] and [can.Model.bind].
*
* Recipe = can.Model.extend({
* destroy : "DELETE /services/recipes/{id}",
* findOne : "/services/recipes/{id}"
* },{})
*
* Recipe.bind("destroyed", function(){
* console.log("a recipe destroyed");
* });
*
* // get a recipe
* Recipe.findOne({id: 5}, function(recipe){
* recipe.bind("destroyed", function(){
* console.log("this recipe destroyed")
* })
* recipe.destroy();
* })
*/
destroy: function( success, error ) {
if(this.isNew()) {
var self = this;
var def = can.Deferred();
def.then(success, error);
return def.done(function(data) {
self.destroyed(data)
}).resolve(self);
}
return makeRequest(this, 'destroy', success, error, 'destroyed');
},
/**
* @description Listen to events on this Model.
* @function can.Model.prototype.bind bind
* @signature `model.bind(eventName, handler)`
* @param {String} eventName The event to bind to.
* @param {function} handler The function to call when the
* event occurs. __handler__ is passed the event and the
* Model instance.
* @return {can.Model} The Model, for chaining.
*
* @body
* `bind(eventName, handler(ev, args...) )` is used to listen
* to events on this model instance. Example:
*
* Task = can.Model.extend()
* var task = new Task({name : "dishes"})
* task.bind("name", function(ev, newVal, oldVal){})
*
* Use `bind` the
* same as [can.Map::bind] which should be used as
* a reference for listening to property changes.
*
* Bind on model can be used to listen to when
* an instance is:
*
* - created
* - updated
* - destroyed
*
* like:
*
* Task = can.Model.extend()
* var task = new Task({name : "dishes"})
*
* task.bind("created", function(ev, newTask){
* console.log("created", newTask)
* })
* .bind("updated", function(ev, updatedTask){
* console.log("updated", updatedTask)
* })
* .bind("destroyed", function(ev, destroyedTask){
* console.log("destroyed", destroyedTask)
* })
*
* // create, update, and destroy
* task.save(function(){
* task.attr('name', "do dishes")
* .save(function(){
* task.destroy()
* })
* });
*
*
* `bind` also extends the inherited
* behavior of [can.Map::bind] to track the number
* of event bindings on this object which is used to store
* the model instance. When there are no bindings, the
* model instance is removed from the store, freeing memory.
*/
_bindsetup: function(){
this.constructor.store[this.__get(this.constructor.id)] = this;
return can.Map.prototype._bindsetup.apply( this, arguments );
},
/**
* @function can.Model.prototype.unbind unbind
* @description Stop listening to events on this Model.
* @signature `model.unbind(eventName[, handler])`
* @param {String} eventName The event to unbind from.
* @param {function} [handler] A handler previously bound with `bind`.
* If __handler__ is not passed, `unbind` will remove all handlers
* for the given event.
* @return {can.Model} The Model, for chaining.
*
* @body
* `unbind(eventName, handler)` removes a listener
* attached with [can.Model::bind].
*
* var handler = function(ev, createdTask){
*
* }
* task.bind("created", handler)
* task.unbind("created", handler)
*
* You have to pass the same function to `unbind` that you
* passed to `bind`.
*
* Unbind will also remove the instance from the store
* if there are no other listeners.
*/
_bindteardown: function(){
delete this.constructor.store[getId(this)];
return can.Map.prototype._bindteardown.apply( this, arguments );;
},
// Change `id`.
___set: function( prop, val ) {
can.Map.prototype.___set.call(this,prop, val)
// If we add an `id`, move it to the store.
if(prop === this.constructor.id && this._bindings){
this.constructor.store[getId(this)] = this;
}
}
});
can.each({
/**
* @function can.Model.makeFindAll
* @parent can.Model.static
*
* @signature `can.Model.makeFindAll: function(findAllData) -> findAll`
*
* Returns the external `findAll` method given the implemented [can.Model.findAllData findAllData] function.
*
* @params {can.Model.findAllData}
*
* [can.Model.findAll] is implemented with a `String`, [can.AjaxSettings ajax settings object], or
* [can.Model.findAllData findAllData] function. If it is implemented as
* a `String` or [can.AjaxSettings ajax settings object], those values are used
* to create a [can.Model.findAllData findAllData] function.
*
* The [can.Model.findAllData findAllData] function is passed to `makeFindAll`. `makeFindAll`
* should use `findAllData` internally to get the raw data for the request.
*
* @return {function(params,success,error):can.Deferred}
*
* Returns function that implements the external API of `findAll`.
*
* @body
*
* ## Use
*
* `makeFindAll` can be used to implement base models that perform special
* behavior. `makeFindAll` is passed a [can.Model.findAllData findAllData] function that retrieves raw
* data. It should return a function that when called, uses
* the findAllData function to get the raw data, convert them to model instances with
* [can.Model.models models].
*
* ## Caching
*
* The following uses `makeFindAll` to create a base `CachedModel`:
*
* CachedModel = can.Model.extend({
* makeFindAll: function(findAllData){
* // A place to store requests
* var cachedRequests = {};
*
* return function(params, success, error){
* // is this not cached?
* if(! cachedRequests[JSON.stringify(params)] ) {
* var self = this;
* // make the request for data, save deferred
* cachedRequests[JSON.stringify(params)] =
* findAllData(params).then(function(data){
* // convert the raw data into instances
* return self.models(data)
* })
* }
* // get the saved request
* var def = cachedRequests[JSON.stringify(params)]
* // hookup success and error
* def.then(success,error)
* return def;
* }
* }
* },{})
*
* The following Todo model will never request the same list of todo's twice:
*
* Todo = CachedModel({
* findAll: "/todos"
* },{})
*
* // widget 1
* Todo.findAll({})
*
* // widget 2
* Todo.findAll({})
*/
makeFindAll : "models",
/**
* @function can.Model.makeFindOne
* @parent can.Model.static
*
* @signature `can.Model.makeFindOne: function(findOneData) -> findOne`
*
* Returns the external `findOne` method given the implemented [can.Model.findOneData findOneData] function.
*
* @params {can.Model.findOneData}
*
* [can.Model.findOne] is implemented with a `String`, [can.AjaxSettings ajax settings object], or
* [can.Model.findOneData findOneData] function. If it is implemented as
* a `String` or [can.AjaxSettings ajax settings object], those values are used
* to create a [can.Model.findOneData findOneData] function.
*
* The [can.Model.findOneData findOneData] function is passed to `makeFindOne`. `makeFindOne`
* should use `findOneData` internally to get the raw data for the request.
*
* @return {function(params,success,error):can.Deferred}
*
* Returns function that implements the external API of `findOne`.
*
* @body
*
* ## Use
*
* `makeFindOne` can be used to implement base models that perform special
* behavior. `makeFindOne` is passed a [can.Model.findOneData findOneData] function that retrieves raw
* data. It should return a function that when called, uses
* the findOneData function to get the raw data, convert them to model instances with
* [can.Model.models models].
*
* ## Caching
*
* The following uses `makeFindOne` to create a base `CachedModel`:
*
* CachedModel = can.Model.extend({
* makeFindOne: function(findOneData){
* // A place to store requests
* var cachedRequests = {};
*
* return function(params, success, error){
* // is this not cached?
* if(! cachedRequests[JSON.stringify(params)] ) {
* var self = this;
* // make the request for data, save deferred
* cachedRequests[JSON.stringify(params)] =
* findOneData(params).then(function(data){
* // convert the raw data into instances
* return self.model(data)
* })
* }
* // get the saved request
* var def = cachedRequests[JSON.stringify(params)]
* // hookup success and error
* def.then(success,error)
* return def;
* }
* }
* },{})
*
* The following Todo model will never request the same todo twice:
*
* Todo = CachedModel({
* findOne: "/todos/{id}"
* },{})
*
* // widget 1
* Todo.findOne({id: 5})
*
* // widget 2
* Todo.findOne({id: 5})
*/
makeFindOne: "model",
makeCreate: "model",
makeUpdate: "model"
}, function( method, name ) {
can.Model[name] = function( oldMethod ) {
return function() {
var args = can.makeArray(arguments),
oldArgs = can.isFunction( args[1] ) ? args.splice( 0, 1 ) : args.splice( 0, 2 ),
def = pipe( oldMethod.apply( this, oldArgs ), this, method );
def.then( args[0], args[1] );
// return the original promise
return def;
};
};
});
can.each([
/**
* @function can.Model.prototype.created created
* @hide
* Called by save after a new instance is created. Publishes 'created'.
* @param {Object} attrs
*/
"created",
/**
* @function can.Model.prototype.updated updated
* @hide
* Called by save after a