UNPKG

todomvc

Version:

> Helping you select an MV\* framework

401 lines (380 loc) 11.9 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"], function(can, Map) { can.each([ can.Map, can.Model ], function(clss){ // in some cases model might not be defined quite yet. if(clss === undefined){ return; } var isObject = function( obj ) { return typeof obj === 'object' && obj !== null && obj; }; can.extend(clss, { /** * @property can.Map.attributes.static.attributes attributes * @parent can.Map.attributes.static * * `can.Map.attributes` is a property that contains key/value pair(s) of an attribute's name and its * respective type for using in [can.Map.attributes.static.convert convert] and [can.Map.prototype.serialize serialize]. * * var Contact = can.Map.extend({ * attributes : { * birthday : 'date', * age: 'number', * name: 'string' * } * }); * */ attributes : {}, /** * @property can.Map.attributes.static.convert convert * @parent can.Map.attributes.static * * You often want to convert from what the observe sends you to a form more useful to JavaScript. * For example, contacts might be returned from the server with dates that look like: "1982-10-20". * We can observe to convert it to something closer to `new Date(1982,10,20)`. * * Convert comes with the following types: * * - __date__ Converts to a JS date. Accepts integers or strings that work with Date.parse * - __number__ An integer or number that can be passed to parseFloat * - __boolean__ Converts "false" to false, and puts everything else through Boolean() * * The following sets the birthday attribute to "date" and provides a date conversion function: * * var Contact = can.Map.extend({ * attributes : { * birthday : 'date' * }, * convert : { * date : function(raw){ * if(typeof raw == 'string'){ * //- Extracts dates formated 'YYYY-DD-MM' * var matches = raw.match(/(\d+)-(\d+)-(\d+)/); * * //- Parses to date object and returns * return new Date(matches[1], * (+matches[2])-1, * matches[3]); * * }else if(raw instanceof Date){ * return raw; * } * } * } * },{}); * * var contact = new Contact(); * * //- calls convert on attribute set * contact.attr('birthday', '4-26-2012') * * contact.attr('birthday'); //-> Date * * If a property is set with an object as a value, the corresponding converter is called with the unmerged data (the raw object) * as the first argument, and the old value (a can.Map) as the second: * * var MyObserve = can.Map.extend({ * attributes: { * nested: "nested" * }, * convert: { * nested: function(data, oldVal) { * if(oldVal instanceof MyObserve) { * return oldVal.attr(data); * } * return new MyObserve(data); * } * } * },{}); * * ## Differences From `attr` * * The way that return values from convertors affect the value of an Observe's property is * different from [can.Map::attr attr]'s normal behavior. Specifically, when the * property's current value is an Observe or List, and an Observe or List is returned * from a convertor, the effect will not be to merge the values into the current value as * if the return value was fed straight into `attr`, but to replace the value with the * new Observe or List completely. Because of this, any bindings you have on the previous * observable object will break. * * If you would rather have the new Observe or List merged into the current value, call * `attr` directly on the property instead of on the Observe: * * @codestart * var Contact = can.Map.extend({ * attributes: { * info: 'info' * }, * convert: { * 'info': function(data, oldVal) { * return data; * } * } * }, {}); * * var alice = new Contact({info: {name: 'Alice Liddell', email: 'alice@liddell.com'}}); * alice.attr(); // {name: 'Alice Liddell', 'email': 'alice@liddell.com'} * alice.info._cid; // '.observe1' * * alice.attr('info', {name: 'Allison Wonderland', phone: '888-888-8888'}); * alice.attr(); // {name: 'Allison Wonderland', 'phone': '888-888-8888'} * alice.info._cid; // '.observe2' * * alice.info.attr({email: 'alice@wonderland.com', phone: '000-000-0000'}); * alice.attr(); // {name: 'Allison Wonderland', email: 'alice@wonderland.com', 'phone': '000-000-0000'} * alice.info._cid; // '.observe2' * @codeend * * ## Assocations and Convert * * If you have assocations defined within your model(s), you can use convert to automatically * call serialize on those models. * * @codestart * var Contact = can.Model.extend({ * attributes : { * tasks: Task * } * }, {}); * * var Task = can.Model.extend({ * attributes : { * due : 'date' * } * },{}); * * var contact = new Contact({ * tasks: [ new Task({ * due: new Date() * }) ] * }); * * contact.serialize(); * //-> { tasks: [ { due: 1333219754627 } ] } * @codeend */ convert: { "date": function( str ) { var type = typeof str; if ( type === "string" ) { str = Date.parse(str); return isNaN(str) ? null : new Date(str); } else if ( type === 'number' ) { return new Date(str) } else { return str } }, "number": function( val ) { return parseFloat(val); }, "boolean": function (val) { if(val === 'false' || val === '0' || !val) { return false; } return true; }, "default": function( val, oldVal, error, type ) { // Convert can.Model types using .model and .models if(can.Map.prototype.isPrototypeOf(type.prototype) && typeof type.model === 'function' && typeof type.models === 'function') { return type[can.isArray(val) ? 'models' : 'model'](val); } if(can.Map.prototype.isPrototypeOf(type.prototype)) { if(can.isArray(val) && typeof type.List === 'function') { return new type.List(val); } return new type(val); } if(typeof type === 'function') { return type(val, oldVal); } var construct = can.getObject(type), context = window, realType; // if type has a . we need to look it up if ( type.indexOf(".") >= 0 ) { // get everything before the last . realType = type.substring(0, type.lastIndexOf(".")); // get the object before the last . context = can.getObject(realType); } return typeof construct == "function" ? construct.call(context, val, oldVal) : val; } }, /** * @property can.Map.attributes.static.serialize serialize * @parent can.Map.attributes.static * * `can.Map.serialize` is an object of name-function pairs that are used to * serialize attributes. * * Similar to [can.Map.attributes.static.convert can.Map.attributes.convert], in that the keys of this object correspond to * the types specified in [can.Map.attributes]. * * By default every attribute will be passed through the 'default' serialization method * that will return the value if the property holds a primitive value (string, number, ...), * or it will call the "serialize" method if the property holds an object with the "serialize" method set. * * For example, to serialize all dates to ISO format: * * @codestart * var Contact = can.Map.extend({ * attributes : { * birthday : 'date' * }, * serialize : { * date : function(val, type){ * return new Date(val).toISOString(); * } * } * },{}); * * var contact = new Contact({ * birthday: new Date("Oct 25, 1973") * }).serialize(); * //-> { "birthday" : "1973-10-25T05:00:00.000Z" } * @codeend * */ serialize: { "default": function( val, type ) { return isObject(val) && val.serialize ? val.serialize() : val; }, "date": function( val ) { return val && val.getTime() } } }); // overwrite setup to do this stuff var oldSetup = clss.setup; /** * @hide * @function can.Map.setup * @parent can.Map.attributes * * `can.Map.static.setup` overrides default `can.Map` setup to provide * functionality for attributes. * */ clss.setup = function(superClass, stat, proto){ var self = this; oldSetup.call(self, superClass, stat, proto); can.each(["attributes"], function( name ) { if (!self[name] || superClass[name] === self[name] ) { self[name] = {}; } }); can.each(["convert", "serialize"], function( name ) { if ( superClass[name] != self[name] ) { self[name] = can.extend({}, superClass[name], self[name]); } }); }; }); /** * @hide * @function can.Map.prototype.convert * @parent can.Map.attributes */ can.Map.prototype.__convert = function(prop, value){ // check if there is a var Class = this.constructor, oldVal = this.attr(prop), type, converter; if(Class.attributes){ // the type of the attribute type = Class.attributes[prop]; converter = Class.convert[type] || Class.convert['default']; } return value === null || !type ? // just use the value value : // otherwise, pass to the converter converter.call(Class, value, oldVal, function() {}, type); }; /** * @function can.Map.prototype.attributes.serialize serialize * @parent can.Map.attributes.prototype * * @description Serializes the observe's properties using * the [can.Map.attributes attribute plugin]. * * @signature `observe.serialize([attrName])` * @param {String} [attrName] If passed, returns only a serialization of the named attribute. * @return {String} A serialization of this Observe. * * @body * You can set the serialization methods similar to the convert methods: * * var Contact = can.Map.extend({ * attributes : { * birthday : 'date' * }, * serialize : { * date : function( val, type ){ * return val.getYear() + * "-" + (val.getMonth() + 1) + * "-" + val.getDate(); * } * } * },{}) * * var contact = new Contact(); * contact.attr('birthday', new Date()); * contact.serialize() * //-> { birthday: 'YYYY-MM-DD' } * * You can also get and serialize an individual property by passing the attribute * name to the `serialize` function. Building on the above demo, we can serialize * the `birthday` attribute only. * * contact.serialize('birthday') //-> 'YYYY-MM-DD' */ can.Map.prototype.serialize = function(attrName, stack) { var where = {}, Class = this.constructor, attrs = {}; stack = can.isArray(stack) ? stack : []; stack.push(this._cid); if(attrName !== undefined){ attrs[attrName] = this[attrName]; } else { attrs = this.__get(); } can.each(attrs, function( val, name ) { var type, converter; // If this is an observe, check that it wasn't serialized earlier in the stack. if(val instanceof can.Map && can.inArray(val._cid, stack) > -1) { // Since this object has already been serialized once, // just reference the id (or undefined if it doesn't exist). where[name] = val.attr('id'); } else { type = Class.attributes ? Class.attributes[name] : 0; converter = Class.serialize ? Class.serialize[type] : 0; // if the value is an object, and has a attrs or serialize function where[name] = val && typeof val.serialize == 'function' ? // call attrs or serialize to get the original data back val.serialize(undefined, stack) : // otherwise if we have a converter converter ? // use the converter converter(val, type) : // or return the val val; } }); return attrName != undefined ? where[attrName] : where; }; return can.Map; });