UNPKG

todomvc

Version:

> Helping you select an MV\* framework

1,558 lines (1,514 loc) 51.7 kB
/*! * 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