Skip to content

Commit

Permalink
Merge branch 'map-define' into list-promise-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
moschel committed Apr 30, 2014
2 parents 6829105 + 5e1a337 commit 7dff78a
Show file tree
Hide file tree
Showing 17 changed files with 422 additions and 65 deletions.
6 changes: 6 additions & 0 deletions map/attributes/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ steal('can/util', 'can/map', 'can/list', function (can, Map) {
can.Map,
can.Model
], function (clss) {

//!steal-remove-start
can.dev.warn("can/map/attributes is a deprecated plugin and will be removed in a future release. "+
"can/map/define provides the same functionality in a more complete API.");
//!steal-remove-end

// in some cases model might not be defined quite yet.
if (clss === undefined) {
return;
Expand Down
9 changes: 8 additions & 1 deletion map/attributes/doc/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@
@group can.Map.attributes.static static
@group can.Map.attributes.prototype prototype

@body

## Deprecation Warning

The attributes plugin (and the setter plugin) has been deprecated in 2.1 in favor of the new [can.Map.prototype.define define] plugin, which provides the same functionality.

## Use

can.Map.attributes is a plugin that helps convert and normalize data being set on an Map
and allows you to specify the way complex types get serialized. The attributes plugin is most
helpful when used with [can.Model] \(because the serialization aids in sending data to a server),
but you can use it with any Map you plan to make instances
from.

@body
There are three important static properties to give the class you want to use attributes with:

- `[can.Map.attributes.static.attributes attributes]` lists the properties that will be normalized
Expand Down
37 changes: 36 additions & 1 deletion map/define/define.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ steal('can/util', 'can/observe', function (can) {
return '' + val;
}
};

// the old type sets up bubbling
var oldType = proto.__type;
proto.__type = function (value, prop) {
Expand Down Expand Up @@ -206,5 +206,40 @@ steal('can/util', 'can/observe', function (can) {
}
}
};

proto.serialize = function () {
var serialized = {},
serializer, val,
serializedVal;
// Go through each property.
for(var attr in this.define){
val = this.attr(attr);
serializer = this.define && this.define[attr] && this.define[attr].serialize;
// skip anything that has serialize: false
if(serializer === false){
continue;
}
// If the value is an `object`, and has an `attrs` or `serialize` function.
serializedVal = serializer? serializer.call(this, val): (can.Map.helpers.isObservable(val) && can.isFunction(val.serialize) ?
// Call `attrs` or `serialize` to get the original data back.
val.serialize() :
// Otherwise return the value.
val);

// if the serializer method returns false, don't include this property
if(serializedVal === false){
continue;
}

serialized[attr] = serializedVal;

can.__reading(this, attr);
}

can.__reading(this, '__keys');

return serialized;
};

return can.Map;
});
37 changes: 37 additions & 0 deletions map/define/define_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -405,4 +405,41 @@ steal("can/map/define", "can/test", function () {
});


test("serialize", function(){
var MyMap = can.Map.extend({
define: {
name: {
serialize: function(){
return false;
}
},
locations: {
serialize: false
},
locationIds: {
get: function(){
var ids = [];
this.attr('locations').each(function(location){
ids.push(location.id);
});
return ids;
},
serialize: function(locationIds){
return locationIds.join(',');
}
}
}
});

var map = new MyMap();
map.attr("locations", [{id: 1, name: "Chicago"}, {id: 2, name: "LA"}]);
equal(map.attr("locationIds").length, 2, "get locationIds");
equal(map.attr("locationIds")[0], 1, "get locationIds index 0");
equal(map.attr("locations")[0].id, 1, "get locations index 0");
var serialized = map.serialize();
equal(serialized.locations, undefined, "locations doesn't serialize");
equal(serialized.locationIds, "1,2", "locationIds serializes");
equal(serialized.name, undefined, "name doesn't serialize");
});

});
23 changes: 23 additions & 0 deletions map/define/doc/TypeConstructor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@function can.Map.prototype.define.TypeConstructor Type
@parent can.Map.prototype.define

Provides a constructor function to be used to convert any value passed into [can.Map::attr attr] into an appropriate value


@signature `constructorFunc`

A constructor function can be provided that is called to convert incoming values set on this property, like:

define: {
prop: {
Type: Person
}
}

@body

Similar to [can.Map.prototype.define.type type], this uppercase version provides a mechanism for converting incoming values to another format or type.

Specifically, this constructor will be invoked any time this property is set, and any data passed into the setter will be passed as arguments for the constructor.

If the call to attr passes an object that is already an instance of the constructor specified with `Type`, no conversion is done.
21 changes: 21 additions & 0 deletions map/define/doc/ValueConstructor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@function can.Map.prototype.define.ValueConstructor Value
@parent can.Map.prototype.define

Provides a constructor function to be used to provide a default value for a certain property of a can.Map. This constructor will be invoked with `new` each time a new instance of the map is created.

@signature `constructorFunc`

A constructor function can be provided that is called to create a default value used for this property, like:

define: {
prop: {
Value: Array
},
person: {
Value: Person
}
}

@body

Similar to [can.Map.prototype.define.value value], this uppercase version provides a mechanism for providing a default value. If the default value is an object, providing a constructor is a good way to ensure a copy is made for each instance.
26 changes: 20 additions & 6 deletions map/define/doc/attrDefinition.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ returns a fresh copy can be provided:
}
}

@option {function} Value Specifies a function that will be called with `new` whos result is
@option {function} Value Specifies a function that will be called with `new` whose result is
set as the initial value of the attribute. For example, if the default value should be a can.List:

define: {
Expand All @@ -48,11 +48,13 @@ The following example converts the `count` property to a number and the `items`

define: {
count: {type: "number"},
items: function(newValue){
if(typeof newValue === "string") {
return newValue.split(",")
} else if( can.isArray(newValue) ) {
return newValue;
items: {
type: function(newValue){
if(typeof newValue === "string") {
return newValue.split(",")
} else if( can.isArray(newValue) ) {
return newValue;
}
}
}
}
Expand Down Expand Up @@ -108,3 +110,15 @@ with [can.Map::removeAttr removeAttr]. The following removes a `modelId` when `m
}
}

@option {can.Map.prototype.define.serialize} serialize A function that specifies what should happen when an attribute is serialized
with [can.Map::serialize serialize]. The following causes the serialized form of the map to contain a locationIds property, which contains comma separated id values derived from the locations property:

define: {
locationIds: {
serialize: function(locationIds){
return locationIds.join(',');
}
}
}

Setting serialize to false for any property means this property will not be part of the serialized object.
17 changes: 16 additions & 1 deletion map/define/doc/define.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
@parent can.Map.plugins

Defines the
type, initial value, and get, set, and remove behavior for attributes
type, initial value, get, set, remove, and serialize behavior for attributes
of a [can.Map].

@option {Object<String,can.Map.prototype.define.attrDefinition>} A map of
Expand Down Expand Up @@ -61,6 +61,21 @@ specifies a Paginate Map:
}
});

## Overview

This plugin is a replacement for the now deprecated [can.Map.attributes attributes] and [can.Map.setter setter] plugins. It intends to provide a single place to define the behavior of all the properties of a can.Map.

Here is the cliffnotes version of this plugin. To define...

* The default value for a property - use [can.Map.prototype.define.value value]
* That default value as a constructor function - use [can.Map.prototype.define.ValueConstructor Value]
* What value is returned when a property is read - use [can.Map.prototype.define.get get]
* Behavior when a property is set - use [can.Map.prototype.define.set set]
* How a property is serialized when [can.Map::serialize serialize] is called on it - use [can.Map.prototype.define.serialize serialize]
* Behavior when a property is removed - use [can.Map.prototype.define.remove remove]
* A custom converter method or a pre-defined standard converter called whenever a property is set - use [can.Map.prototype.define.type type]
* That custom converter method as a constructor function - use [can.Map.prototype.define.TypeConstructor Type]

## Demo

The following shows picking cars by make / model / year:
Expand Down
52 changes: 51 additions & 1 deletion map/define/doc/get.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,52 @@
@function can.Map.prototype.define.get get
@parent can.Map.prototype.define
@parent can.Map.prototype.define

Specify what happens when a certain property is read on a map.


@signature `get( )`

A get function defines the behavior of what happens when a value is read on a
[can.Map]. It is typically used to provide properties that derive their value from other properties of the map.

@return {*} Anything can be returned from a getter.

@body

## Use

Getter methods are useful for defining virtual properties on a map. These are properties that don't actually store any value, but derive their value from some other properties on the map.

Whenever a getter is provided, it is wrapped in a [can.compute], which ensures that whenever its dependent properties change, a change event will fire for this property also.

@codestart
var Person = can.Map.extend({
define: {
fullName: {
get: function () {
return this.attr("first") + " " + this.attr("last");
}
}
}
});

var p = new Person({first: "Justin", last: "Meyer"});

p.attr("fullName"); // "Justin Meyer"
@codeend

Below is another example of using get to create a dependent "virtual" property. This map has a locations property, which is a can.List containing location objects. Another property called locationIds is required, which is just an array of ids from each of the locations in the real location list. The value of locationIds is tied to locations, so a getter is useful. A user could bind to 'locationIds' and its event handler would be triggered if new locations were added, causing a change in the locationIds array.

var Store = can.Map.extend({
define: {
locationIds: {
get: function(){
var ids = [];
this.attr('locations').each(function(location){
ids.push(location.id);
});
return ids;
}
}
}
});
46 changes: 46 additions & 0 deletions map/define/doc/serialize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@function can.Map.prototype.define.serialize serialize
@parent can.Map.prototype.define

Called when an attribute is removed.

@signature `serializer( currentValue )`

@return {*|false} If `false` is returned, the value is not serialized.

@body

## Use

[can.Map::serialize serialize] is useful for serializing a can.Map instance into a more JSON-friendly form. This can be used for many reasons, including saving a can.Model instance on the server or serializing can.route's internal can.Map for display in the hash or pushstate URL.

The serialize property allows an opportunity to define how each attribute will behave when the map is serialized. This can be useful for:

* serializing complex types like dates, arrays, or objects into string formats
* causing certain properties to be ignored when serialize is called

The following causes a locationIds property to be serialized into the comma separated ID values of the location property on this map:

define: {
locationIds: {
serialize: function(){
var ids = [];
this.attr('locations').each(function(location){
ids.push(location.id);
});
return ids.join(',');
}
}
}

Setting serialize to false for any property means this property will not be part of the serialized object. For example, if the property numPages is not greater than zero, the following example won't include it in the serialized object.

define: {
prop: {
numPages: function( num ){
if(num <= 0) {
return false;
}
return num;
}
}
}
Loading

0 comments on commit 7dff78a

Please sign in to comment.