Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attributes automatic conversion to Map/Model specified type #293

Merged
merged 2 commits into from Oct 11, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions map/attributes/attributes-assocations.html
Expand Up @@ -64,7 +64,7 @@
};

// A task model that has a date
can.Model("Task",{
var Task = can.Model({
attributes : {
due : 'date'
}
Expand All @@ -76,10 +76,10 @@
});

// A contact model that has many tasks
can.Model("Contact",{
var Contact = can.Model({
attributes : {
birthday : 'date',
tasks: "Task.models"
tasks: Task
},
findAll : "/contacts.json"
},{
Expand Down
85 changes: 51 additions & 34 deletions map/attributes/attributes.js
@@ -1,5 +1,4 @@
steal('can/util', 'can/map', function(can, Observe) {

steal('can/util', '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){
Expand All @@ -8,15 +7,15 @@ can.each([ can.Map, can.Model ], function(clss){
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',
Expand All @@ -27,13 +26,13 @@ can.each([ can.Map, can.Model ], function(clss){
*
*/
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".
* 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:
Expand Down Expand Up @@ -69,13 +68,13 @@ can.each([ can.Map, can.Model ], function(clss){
* var contact = new Contact();
*
* //- calls convert on attribute set
* contact.attr('birthday', '4-26-2012')
* 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"
Expand All @@ -91,9 +90,9 @@ can.each([ can.Map, can.Model ], function(clss){
* },{});
*
* ## 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
* 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
Expand All @@ -102,7 +101,7 @@ can.each([ can.Map, can.Model ], function(clss){
*
* 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: {
Expand All @@ -114,15 +113,15 @@ can.each([ can.Map, can.Model ], function(clss){
* }
* }
* }, {});
*
*
* 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'
Expand All @@ -132,11 +131,11 @@ can.each([ can.Map, can.Model ], function(clss){
*
* 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.models"
* tasks: Task
* }
* }, {});
*
Expand All @@ -151,8 +150,8 @@ can.each([ can.Map, can.Model ], function(clss){
* due: new Date()
* }) ]
* });
*
* contact.serialize();
*
* contact.serialize();
* //-> { tasks: [ { due: 1333219754627 } ] }
* @codeend
*/
Expand All @@ -178,9 +177,27 @@ can.each([ can.Map, can.Model ], function(clss){
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 .
Expand All @@ -195,14 +212,14 @@ can.each([ can.Map, can.Model ], function(clss){
* @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
* `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
* 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, ...),
* 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:
Expand All @@ -218,8 +235,8 @@ can.each([ can.Map, can.Model ], function(clss){
* }
* }
* },{});
*
* var contact = new Contact({
*
* var contact = new Contact({
* birthday: new Date("Oct 25, 1973")
* }).serialize();
* //-> { "birthday" : "1973-10-25T05:00:00.000Z" }
Expand All @@ -235,10 +252,10 @@ can.each([ can.Map, can.Model ], function(clss){
}
}
});

// overwrite setup to do this stuff
var oldSetup = clss.setup;

/**
* @hide
* @function can.Map.setup
Expand Down Expand Up @@ -277,16 +294,16 @@ can.Map.prototype.__convert = function(prop, value){
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 ?

return value === null || !type ?
// just use the value
value :
value :
// otherwise, pass to the converter
converter.call(Class, value, oldVal, function() {}, type);
};
Expand All @@ -295,7 +312,7 @@ can.Map.prototype.__convert = function(prop, value){
* @function can.Map.prototype.attributes.serialize serialize
* @parent can.Map.attributes.prototype
*
* @description Serializes the observe's properties using
* @description Serializes the observe's properties using
* the [can.Map.attributes attribute plugin].
*
* @signature `observe.serialize([attrName])`
Expand Down Expand Up @@ -351,7 +368,7 @@ can.Map.prototype.serialize = function(attrName, stack) {
// 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;
Expand Down
70 changes: 59 additions & 11 deletions map/attributes/attributes.md
Expand Up @@ -157,24 +157,72 @@ as well. Lastly, `serialize` is invoked converting the new attributes to raw ty
//-> { 'birthday': '11-29-1983', 'weight': '300' }



## Converter functions

Another common case is to create converter functions (`function(value, oldValue) {}`) that return a converted value:

var ValueMap = can.Map.extend({
attributes: {
value: function(orig) {
return orig * 100;
}
}
},{});

console.log(new ValueMap({ value: 0.83 }).attr('value'));


## Associations

Attribute type values can also represent the name of a function. The most common case this is used is for associated data.
The attribute plugin also allows setting up data associations between Maps or Models. This means
that nested data structures can be automatically converted into their Map or Model (using `Model.models`) representations by passing them as the attribute.
If the value to convert is an array it will be converted into its `can.Map.List` or `can.Model.List` (using `can.Model.models`) representation:

For example, a `Deliverable` might have many tasks and an owner (which is a Person). The attributes property might look like:
var Sword = can.Model.extend({
findAll: 'GET /swords'
}, {
getPower: function() {
return this.attr('power') * 100;
}
});

var Deliverable = new can.Map.extend({
attributes : {
tasks : "App.Models.Task.models"
owner: "App.Models.Person.model"
var Level = can.Model.extend({
findAll: 'GET /levels'
}, {
getName: function() {
return 'Level: ' + this.attr('name');
}
},{});
});

var Zelda = can.Model.extend({
findOne: 'GET /zelda/{id}'
attributes: {
sword: Sword,
levelsCompleted: Level
}
},{});

This points tasks and owner properties to use _Task_ and _Person_ to convert the raw data into an array of Tasks and a Person.

It's important to note that the full names of the models themselves are _App.Models.Task_ and _App.Models.Person_. The `.model`
and `.models` parts are appended for the benefit of convert to identify the types as models.
Assuming that `Zelda.findOne({ id: 'link' })` will return something like:

{
sword: {
name: 'Wooden Sword',
power: 0.2
},
levelsCompleted : [
{id: 1, name: 'Aquamentus'},
{id: 2, name: 'Dodongo'}
]
}

The converted data will contain a list or Levels and a sword Model:

Zelda.findOne({ id: 'link' }).then(function(link) {
console.log(link.attr('sword').getPower()); // -> 20
console.log(link.attr('levelsCompleted')[0].getName());
// -> 'Level: Aquamentus'
});

### Demo

Expand Down