diff --git a/list/list.js b/list/list.js index 870fd042054..0b967fe1645 100644 --- a/list/list.js +++ b/list/list.js @@ -88,6 +88,7 @@ steal("can/util", "can/map", "can/map/bubble.js",function (can, Map, bubble) { this.length = 0; can.cid(this, ".map"); this._init = 1; + this._computedBindings = {}; this._setupComputes(); instances = instances || []; var teardownMapping; diff --git a/map/define/define.js b/map/define/define.js index 6e21e1f62ba..552346f8bbc 100644 --- a/map/define/define.js +++ b/map/define/define.js @@ -30,12 +30,50 @@ steal('can/util', 'can/observe', function (can) { var oldSetupDefaults = can.Map.prototype._setupDefaults; - can.Map.prototype._setupDefaults = function () { + can.Map.prototype._setupDefaults = function (obj) { var defaults = oldSetupDefaults.call(this), - Map = this.constructor; + propsCommittedToAttr = {}, + Map = this.constructor, + originalGet = this._get; + + // Overwrite this._get with a version that commits defaults to + // this.attr() as needed. Because calling this.attr() for each individual + // default would be expensive. + this._get = function (originalProp) { + + // If a this.attr() was called using dot syntax (e.g number.0), + // disregard everything after the "." until we call the + // original this._get(). + prop = (originalProp.indexOf('.') !== -1 ? + originalProp.substr(0, originalProp.indexOf('.')) : + prop); + + // If this property has a default and we haven't yet committed it to + // this.attr() + if ((prop in defaults) && ! (prop in propsCommittedToAttr)) { + + // Commit the property's default so that it can be read in + // other defaultGenerators. + this.attr(prop, defaults[prop]); + + // Make not so that we don't commit this property again. + propsCommittedToAttr[prop] = true; + } + + return originalGet.apply(this, arguments); + }; + for (var prop in Map.defaultGenerators) { - defaults[prop] = Map.defaultGenerators[prop].call(this); + // Only call the prop's value method if the property wasn't provided + // during instantiation. + if (! obj || ! (prop in obj)) { + defaults[prop] = Map.defaultGenerators[prop].call(this); + } } + + // Replace original this.attr + this._get = originalGet; + return defaults; }; diff --git a/map/define/define_test.js b/map/define/define_test.js index a064fa5d33c..337561e15e8 100644 --- a/map/define/define_test.js +++ b/map/define/define_test.js @@ -381,11 +381,11 @@ steal("can/map/define", "can/test", function () { }); - + test("getter with initial value", function(){ - + var compute = can.compute(1); - + var Grabber = can.Map.extend({ define: { vals: { @@ -400,14 +400,14 @@ steal("can/map/define", "can/test", function () { } } }); - + var g = new Grabber(); // This assertion doesn't mean much. It's mostly testing // that there were no errors. equal(g.attr("vals").length,0,"zero items in array" ); - + }); - + test("serialize basics", function(){ var MyMap = can.Map.extend({ @@ -445,21 +445,21 @@ steal("can/map/define", "can/test", function () { } } }); - + var map = new MyMap({name: "foo"}); 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"); - + equal(serialized.bared, "foo+bar", "true adds computed props"); equal(serialized.ignored, undefined, "computed props are not serialized by default"); - + }); test("serialize context", function(){ @@ -476,10 +476,10 @@ steal("can/map/define", "can/test", function () { serialize: function(){ serializeContext = this; can.Map.prototype.serialize.apply(this, arguments); - + } }); - + var map = new MyMap(); map.serialize(); equal(context, map); @@ -492,40 +492,40 @@ steal("can/map/define", "can/test", function () { define: { name: { value: 'John Galt', - + get: function(obj){ contexts.get = this; return obj; }, - + remove: function(obj){ contexts.remove = this; return obj; }, - + set: function(obj){ contexts.set = this; return obj; }, - + serialize: function(obj){ contexts.serialize = this; return obj; }, - + type: function(val){ contexts.type = this; return val; } } - + } }); - + var map = new MyMap(); map.serialize(); map.removeAttr('name'); - + equal(contexts.get, map); equal(contexts.remove, map); equal(contexts.set, map); @@ -533,5 +533,61 @@ steal("can/map/define", "can/test", function () { equal(contexts.type, map); }); + test("value generator is not called if default passed", function () { + var TestMap = can.Map.extend({ + define: { + foo: { + value: function () { + throw '"foo"\'s value method should not be called.'; + } + } + } + }); + + var tm = new TestMap({ foo: 'baz' }); + + equal(tm.attr('foo'), 'baz'); + }); + + test("value generator can read other properties", function () { + var NumbersMap = can.Map.extend({ + numbers: [1, 2, 3], + define: { + definedNumbers: { + value: [4, 5, 6] + }, + generatedNumbers: { + value: function () { + return new can.List([7, 8, 9]); + } + }, + firstNumber: { + value: function () { + return this.attr('numbers.0'); + } + }, + middleNumber: { + value: function () { + return this.attr('definedNumbers.1'); + } + }, + lastNumber: { + value: function () { + return this.attr('generatedNumbers.2'); + } + } + } + }); + + var n = NumbersMap(); + var prefix = 'was able to read dependent value from '; + + equal(n.attr('firstNumber'), 1, + prefix + 'traditional can.Map style property definition'); + equal(n.attr('middleNumber'), 5, + prefix + 'Define plugin style default property definition'); + equal(n.attr('lastNumber'), 9, + prefix + 'Define plugin style generated default property definition'); + }); }); \ No newline at end of file diff --git a/map/lazy/lazy.js b/map/lazy/lazy.js index 257e4a329d8..b3acf406e3c 100644 --- a/map/lazy/lazy.js +++ b/map/lazy/lazy.js @@ -21,6 +21,7 @@ steal('can/util', './bubble.js', 'can/map', 'can/list', './nested_reference.js', can.cid(this, ".lazyMap"); // Sets all `attrs`. this._init = 1; + this._computedBindings = {}; this._setupComputes(); var teardownMapping = obj && can.Map.helpers.addToMap(obj, this); diff --git a/map/map.js b/map/map.js index 52a8a2c19e5..55d4647c259 100644 --- a/map/map.js +++ b/map/map.js @@ -267,8 +267,10 @@ steal('can/util', 'can/util/bind','./bubble.js', 'can/construct', 'can/util/batc can.cid(this, ".map"); // Sets all `attrs`. this._init = 1; - // It's handy if we pass this to comptues, because computes can have a default value. - var defaultValues = this._setupDefaults(); + this._computedBindings = {}; + + // It's handy if we pass this to computes, because computes can have a default value. + var defaultValues = this._setupDefaults(obj); this._setupComputes(defaultValues); var teardownMapping = obj && can.Map.helpers.addToMap(obj, this); @@ -288,7 +290,6 @@ steal('can/util', 'can/util/bind','./bubble.js', 'can/construct', 'can/util/batc // Sets up computed properties on a Map. _setupComputes: function () { var computes = this.constructor._computes; - this._computedBindings = {}; for (var i = 0, len = computes.length, prop; i < len; i++) { prop = computes[i]; diff --git a/map/map_benchmark.js b/map/map_benchmark.js index 080dfdc9f71..90b3e71a5e5 100644 --- a/map/map_benchmark.js +++ b/map/map_benchmark.js @@ -17,4 +17,22 @@ steal('can/map', 'can/list', 'can/test/benchmarks.js', function (Map, List, benc map = new can.Map(); map.attr('obj', objects); }); + + var NumbersMap; + benchmarks.add('Overwriting defaults', function () { + NumbersMap = can.Map.extend({ + numbers: [1, 2, 3, 4, 5, 6], + foo: 'string', + bar: {}, + zed: false + }); + }, function () { + new NumbersMap(); + new NumbersMap({ + numbers: ['a', 'b', 'c', 'd'] + }); + new NumbersMap({ + foo: 'blah blah blah' + }); + }); });