From 995aea4151e3f76e0e138887957e8bb817311a36 Mon Sep 17 00:00:00 2001 From: Justin Meyer Date: Wed, 30 Apr 2014 20:19:23 -0500 Subject: [PATCH] fixes serialize problems with define and attributes working at the same time --- map/attributes/attributes.js | 76 +++++++++++++++--------------------- map/backup/backup_test.js | 5 +++ map/define/define.js | 66 +++++++++++++++++-------------- map/define/define_test.js | 2 +- map/map.js | 51 ++++++++++++++++++++---- map/map_test.js | 31 ++++++++++----- 6 files changed, 138 insertions(+), 93 deletions(-) diff --git a/map/attributes/attributes.js b/map/attributes/attributes.js index 67a10d15e48..347d53a41e9 100644 --- a/map/attributes/attributes.js +++ b/map/attributes/attributes.js @@ -1,14 +1,15 @@ steal('can/util', 'can/map', 'can/list', function (can, Map) { + + //!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 + can.each([ 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; @@ -123,49 +124,34 @@ steal('can/util', 'can/map', 'can/list', function (can, Map) { } return value === null || !type ? value : converter.call(Class, value, oldVal, function () {}, type); }; - can.List.prototype.serialize = function (attrName, stack) { - return can.makeArray(can.Map.prototype.serialize.apply(this, arguments)); + + var oldSerialize = can.Map.helpers._serialize; + can.Map.helpers._serialize = function(map, name, val){ + + var constructor = map.constructor, + type = constructor.attributes ? constructor.attributes[name] : 0, + converter = constructor.serialize ? constructor.serialize[type] : 0; + + return val && typeof val.serialize === 'function' ? + // call attrs or serialize to get the original data back + oldSerialize.apply(this, arguments) : + // otherwise if we have a converter + converter ? + // use the converter + converter(val, type) : + // or return the val + oldSerialize.apply(this, arguments); }; - 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]; + // add support for single value serialize + var mapSerialize = can.Map.prototype.serialize; + can.Map.prototype.serialize = function (attrName) { + var baseResult = mapSerialize.apply(this, arguments); + if(attrName){ + return baseResult[attrName]; } else { - attrs = this.__get(); - } - can.each(attrs, function (val, name) { - can.__reading(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; - } - }); - - can.__reading(this, '__keys'); - - if (typeof attrs.length !== 'undefined') { - where.length = attrs.length; + return baseResult; } - return attrName !== undefined ? where[attrName] : where; }; + return can.Map; }); diff --git a/map/backup/backup_test.js b/map/backup/backup_test.js index e752709e3bf..e8e13242c2b 100644 --- a/map/backup/backup_test.js +++ b/map/backup/backup_test.js @@ -63,6 +63,7 @@ steal("can/map/backup", "can/model", "can/test", function () { recipe.restore(true); ok(!recipe.isDirty(true), 'cleaned all of recipe and its associations'); }); + test('backup restore nested observables', function () { var observe = new can.Map({ nested: { @@ -72,15 +73,19 @@ steal("can/map/backup", "can/model", "can/test", function () { equal(observe.attr('nested') .attr('test'), 'property', 'Nested object got converted'); observe.backup(); + observe.attr('nested') .attr('test', 'changed property'); + equal(observe.attr('nested') .attr('test'), 'changed property', 'Nested property changed'); + ok(observe.isDirty(true), 'Observe is dirty'); observe.restore(true); equal(observe.attr('nested') .attr('test'), 'property', 'Nested object got restored'); }); + test('backup removes properties that were added (#607)', function () { var map = new can.Map({}); map.backup(); diff --git a/map/define/define.js b/map/define/define.js index fa3b99705e1..ab5014c578b 100644 --- a/map/define/define.js +++ b/map/define/define.js @@ -127,7 +127,7 @@ steal('can/util', 'can/observe', function (can) { } }, 'number': function (val) { - return parseFloat(val); + return +(val); }, 'boolean': function (val) { if (val === 'false' || val === '0' || !val) { @@ -206,38 +206,46 @@ steal('can/util', 'can/observe', function (can) { } } }; - - proto.serialize = function () { - var serialized = {}, - serializer, val, - serializedVal; + // Overwrite the invidual property serializer b/c we will overwrite it. + var oldSingleSerialize = can.Map.helpers._serialize; + can.Map.helpers._serialize = function(map, name, val){ + return serializeProp(map, name, val); + }; + // If the map has a define serializer for the given attr, run it. + var serializeProp = function(map, attr, val) { + var serializer = map.define && map.define[attr] && map.define[attr].serialize; + if(serializer === undefined) { + return oldSingleSerialize.apply(this, arguments); + } else if(serializer !== false){ + return typeof serializer === "function" ? serializer.call(this, val, attr): oldSingleSerialize.apply(this, arguments); + } + }; + + // Overwrite serialize to add in any missing define serialized properties. + var oldSerialize = proto.serialize; + proto.serialize = function (property) { + var serialized = oldSerialize.apply(this, arguments); + if(property){ + return serialized; + } + // add in properties not already serialized + + var serializer, + val; // 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; + // if it's not already defined + if(!(attr in serialized)) { + // check there is a serializer so we aren't doing extra work on serializer:false + serializer = this.define && this.define[attr] && this.define[attr].serialize; + if(serializer) { + val = serializeProp(this, attr, this.attr(attr)); + if(val !== undefined) { + serialized[attr] = val; + } + } } - - serialized[attr] = serializedVal; - - can.__reading(this, attr); } - - can.__reading(this, '__keys'); - return serialized; }; diff --git a/map/define/define_test.js b/map/define/define_test.js index d7868d820d2..83762fb74ee 100644 --- a/map/define/define_test.js +++ b/map/define/define_test.js @@ -410,7 +410,7 @@ steal("can/map/define", "can/test", function () { define: { name: { serialize: function(){ - return false; + return; } }, locations: { diff --git a/map/map.js b/map/map.js index 9f2136cdf5d..6ef4882dea8 100644 --- a/map/map.js +++ b/map/map.js @@ -24,6 +24,9 @@ steal('can/util', 'can/util/bind','./bubble.js', 'can/construct', 'can/util/batc var getMapFromObject = function (obj) { return madeMap && madeMap[obj._cid] && madeMap[obj._cid].instance; }; + // A temporary map of Maps + var serializeMap = null; + /** * @add can.Map @@ -167,21 +170,53 @@ steal('can/util', 'can/util/bind','./bubble.js', 'can/construct', 'can/util/batc // ### can.Map.helpers.serialize // Serializes a Map or Map.List serialize: function (map, how, where) { + var cid = can.cid(map), + firstSerialize = false; + if(!serializeMap) { + firstSerialize = true; + // Serialize might call .attr() so we need to keep different map + serializeMap = { + attr: {}, + serialize: {} + }; + } + serializeMap[how][cid] = where; // Go through each property. map.each(function (val, name) { // If the value is an `object`, and has an `attrs` or `serialize` function. - where[name] = Map.helpers.isObservable(val) && can.isFunction(val[how]) ? - // Call `attrs` or `serialize` to get the original data back. - val[how]() : - // Otherwise return the value. - val; - - can.__reading(map, name); + var result, + isObservable = Map.helpers.isObservable(val), + serialized = isObservable && serializeMap[how][can.cid(val)]; + if( serialized ) { + result = serialized; + } else { + if(how === "serialize") { + result = Map.helpers._serialize(map, name, val); + } else { + result = Map.helpers._getValue(map, name, val, how); + } + } + // this is probably removable + if(result !== undefined){ + where[name] = result; + } }); can.__reading(map, '__keys'); - + if(firstSerialize) { + serializeMap = null; + } return where; + }, + _serialize: function(map, name, val){ + return Map.helpers._getValue(map, name, val, "serialize"); + }, + _getValue: function(map, name, val, how){ + if( Map.helpers.isObservable(val) ) { + return val[how](); + } else { + return val; + } } }, /** diff --git a/map/map_test.js b/map/map_test.js index 00f8537f056..5122dac8287 100644 --- a/map/map_test.js +++ b/map/map_test.js @@ -1,7 +1,7 @@ /* jshint asi:true*/ -steal("can/map", "can/compute", "can/test", "can/list", function (undefined) { +steal("can/map", "can/compute", "can/test", "can/list", function(){ - module('can/map') + module('can/map'); test("Basic Map", 4, function () { @@ -11,10 +11,10 @@ steal("can/map", "can/compute", "can/test", "can/list", function (undefined) { }); state.bind("change", function (ev, attr, how, val, old) { - equal(attr, "category", "correct change name") - equal(how, "set") - equal(val, 6, "correct") - equal(old, 5, "correct") + equal(attr, "category", "correct change name"); + equal(how, "set"); + equal(val, 6, "correct"); + equal(old, 5, "correct"); }); state.attr("category", 6); @@ -63,16 +63,16 @@ steal("can/map", "can/compute", "can/test", "can/list", function (undefined) { var state2 = new can.Map({ "key.with.dots": 4, key: { - with: { + "with": { someValue: 20 } } - }) + }); state.removeAttr("key.with.dots"); state2.removeAttr("key.with.someValue"); deepEqual(can.Map.keys(state), ["productType"], "one property"); deepEqual(can.Map.keys(state2), ["key.with.dots", "key"], "two properties"); - deepEqual(can.Map.keys(state2.key.with), [], "zero properties"); + deepEqual( can.Map.keys( state2.key["with"] ) , [], "zero properties"); }); test("nested event handlers are not run by changing the parent property (#280)", function () { @@ -227,5 +227,16 @@ steal("can/map", "can/compute", "can/test", "can/list", function (undefined) { ok(!map._computedBindings.name.handler, 'computed property handler removed'); }); - + test("serializing cycles", function(){ + var map1 = new can.Map({name: "map1"}); + var map2 = new can.Map({name: "map2"}); + + map1.attr("map2", map2); + map2.attr("map1", map1); + + var res = map1.serialize(); + equal(res.name, "map1"); + equal(res.map2.name, "map2"); + }); + });