Skip to content

Commit

Permalink
Add a "compute" type converter to the Define plugin
Browse files Browse the repository at this point in the history
Closes #1409
  • Loading branch information
Chris Gomez committed Feb 25, 2015
1 parent ad26cde commit 301ba7d
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 24 deletions.
57 changes: 48 additions & 9 deletions map/define/define.js
Expand Up @@ -43,9 +43,9 @@ steal('can/util', 'can/observe', function (can) {
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.
// Temporarily overwrite this._get with a version that commits defaults to
// this.attr() as they are used in other generated defaults. 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),
Expand All @@ -63,7 +63,7 @@ steal('can/util', 'can/observe', function (can) {
// other defaultGenerators.
this.attr(prop, defaults[prop]);

// Make not so that we don't commit this property again.
// Make note so that we don't commit this property again.
propsCommittedToAttr[prop] = true;
}

Expand All @@ -86,7 +86,7 @@ steal('can/util', 'can/observe', function (can) {


var proto = can.Map.prototype,
oldSet = proto.__set;
__set = proto.__set;
proto.__set = function (prop, value, current, success, error) {
//!steal-remove-start
var asyncTimer;
Expand Down Expand Up @@ -124,7 +124,7 @@ steal('can/util', 'can/observe', function (can) {
var setterCalled = false,

setValue = setter.call(this, value, function (value) {
oldSet.call(self, prop, value, current, success, errorCallback);
__set.call(self, prop, value, current, success, errorCallback);
setterCalled = true;
//!steal-remove-start
clearTimeout(asyncTimer);
Expand All @@ -146,7 +146,7 @@ steal('can/util', 'can/observe', function (can) {
return;
} else {
if (!setterCalled) {
oldSet.call(self, prop,
__set.call(self, prop,
// if no arguments, we are side-effects only
setter.length === 0 && setValue === undefined ? value : setValue,
current,
Expand All @@ -158,12 +158,32 @@ steal('can/util', 'can/observe', function (can) {
}

} else {
oldSet.call(self, prop, value, current, success, errorCallback);
__set.call(self, prop, value, current, success, errorCallback);
}

return this;
};

var ___set = proto.___set;
proto.___set = function (prop, val) {
var type = getPropDefineBehavior("type", prop, this.define);

// If this property's "type" is not "compute", set the value as always
if (type !== 'compute' || ! val.isComputed) {
return ___set.apply(this, arguments);
}

// If this property's "type" is "compute", treat the compute like
// a can.Map does on setup
// 1) Save the compute so that it can be written to in the original
// ___set
this[prop] = val;
// 2) Track the number of uses
this._trackCompute(prop);

return ___set.call(this, prop, val());
};

var converters = {
'date': function (str) {
var type = typeof str;
Expand Down Expand Up @@ -197,9 +217,28 @@ steal('can/util', 'can/observe', function (can) {
},
'string': function (val) {
return '' + val;
},
'compute': function (val, prop) {

// Don't convert properties that are meant to be computes
// if they're already computes and the new value is not
// a compute.
if ((this[prop] && this[prop].isComputed) &&
! val.isComputed) {
return val;
}

// If this is the first time this is being set and it's
// not a compute, make it one
if (! val.isComputed) {
return can.compute(val);
}

// If the user passed a compute, do nothing
return val;
}
};

// the old type sets up bubbling
var oldType = proto.__type;
proto.__type = function (value, prop) {
Expand Down
110 changes: 101 additions & 9 deletions map/define/define_test.js
Expand Up @@ -225,23 +225,26 @@ steal("can/map/define", "can/route", "can/test", "steal-qunit", function () {

var Typer = can.Map.extend({
define: {
date: { type: 'date' },
string: {type: 'string'},
number: { type: 'number' },
'boolean': { type: 'boolean' },
htmlbool: { type: 'htmlbool' },
leaveAlone: { type: '*' }
date: { type: 'date' },
string: { type: 'string' },
number: { type: 'number' },
'boolean': { type: 'boolean' },
htmlbool: { type: 'htmlbool' },
leaveAlone: { type: '*' },
computed: { type: 'compute' }
}
});
var obj = {};
var computedVal = can.compute(0);

var t = new Typer({
date: 1395896701516,
string: 5,
number: '5',
'boolean': 'false',
htmlbool: "",
leaveAlone: obj
leaveAlone: obj,
computed: computedVal
});

ok(t.attr("date") instanceof Date, "converted to date");
Expand All @@ -255,10 +258,17 @@ steal("can/map/define", "can/route", "can/test", "steal-qunit", function () {
equal(t.attr("htmlbool"), true, "converted to htmlbool");

equal(t.attr("leaveAlone"), obj, "left as object");

equal(t.attr("computed"), 0, "computed value returned");

t.attr({
'number': '15'
'number': '15',
computed: 1
});
ok(t.attr("number") === 15, "converted to number");

equal(t.attr("number"), 15, "converted to number");
equal(t.computed, computedVal, 'saved to map as a compute');
equal(t.attr("computed"), 1, "compute updated via attr");

});

Expand Down Expand Up @@ -686,4 +696,86 @@ steal("can/map/define", "can/route", "can/test", "steal-qunit", function () {
ok(nested.attr('examples.two.deep') instanceof Example);
});

test('setting a value of a property with type "compute" triggers change events', function () {

var handler;
var message = 'The change event passed the correct {prop} when set with {method}';
var createChangeHandler = function (expectedOldVal, expectedNewVal, method) {
return function (ev, newVal, oldVal) {
var subs = { prop: 'newVal', method: method };
equal(newVal, expectedNewVal, can.sub(message, subs));
subs.prop = 'oldVal';
equal(oldVal, expectedOldVal, can.sub(message, subs));
};
};

var count = 0;

var ComputableMap = can.Map.extend({
define: {
computed: {
type: 'compute',
}
},
alsoComputed: can.compute(function (newVal) {
if (newVal) {
count = newVal;
return;
}

return count;
})
});

var computed = can.compute(0);

var m1 = new ComputableMap({
computed: computed
});

equal(m1.attr('computed'), 0, 'm1 is 1');

handler = createChangeHandler(0, 1, ".attr('computed', newVal)");
m1.bind('alsoComputed', handler);
m1.attr('alsoComputed', 1);
m1.unbind('alsoComputed', handler);

handler = createChangeHandler(0, 1, ".attr('computed', newVal)");
m1.bind('computed', handler);
m1.attr('computed', 1);
m1.unbind('computed', handler);

handler = createChangeHandler(1, 2, "computed()");
m1.bind('computed', handler);
computed(2);
m1.unbind('computed', handler);
});

test('replacing the compute on a property with type "compute"', function () {
var compute1 = can.compute(0);
var compute2 = can.compute(1);

var ComputableMap = can.Map.extend({
define: {
computable: {
type: 'compute'
}
}
});

var m = new ComputableMap();

m.attr('computable', compute1);

equal(m.computable, compute1, 'compute1 saved to map');

equal(m.attr('computable'), 0, 'compute1 readable via .attr()');

m.attr('computable', compute2);

equal(m.computable, compute2, 'compute2 saved to map');

equal(m.attr('computable'), 1, 'compute2 readable via .attr()');
});

});
20 changes: 14 additions & 6 deletions map/map.js
Expand Up @@ -300,14 +300,22 @@ steal('can/util', 'can/util/bind','./bubble.js', 'can/construct', 'can/util/batc

for (var i = 0, len = computes.length, prop; i < len; i++) {
prop = computes[i];
// Make the context of the compute the current Map
this[prop] = this[prop].clone(this);
// Keep track of computed properties
this._computedBindings[prop] = {
count: 0
};
// Make the context of the compute the current Map, if it's
// already a defined property of this Map
if (this[prop]) {
this[prop] = this[prop].clone(this);
}
this._trackCompute(prop);
}
},

_trackCompute: function (prop) {
// Keep track of computed properties
this._computedBindings[prop] = {
count: 0
};
},

_setupDefaults: function(){
return this.constructor.defaults || {};
},
Expand Down

0 comments on commit 301ba7d

Please sign in to comment.