From 6ca80f438e5936b7966758b6bac79fbc7c74158a Mon Sep 17 00:00:00 2001 From: Mat Tyndall Date: Fri, 5 Dec 2014 17:15:53 -0800 Subject: [PATCH 1/3] added type attr for derived properties to allow for derived states --- ampersand-state.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ampersand-state.js b/ampersand-state.js index 2461296..cc59b87 100644 --- a/ampersand-state.js +++ b/ampersand-state.js @@ -391,9 +391,13 @@ _.extend(Base.prototype, BBEvents, { var newVal = def.fn.call(self); - if (self._cache[name] !== newVal || !def.cache) { + var isEqual = self._getCompareForType(def.type); + var currentVal = self._cache[name]; + var hasChanged = !isEqual(currentVal, newVal, name); + + if (hasChanged || !def.cache) { if (def.cache) { - self._previousAttributes[name] = self._cache[name]; + self._previousAttributes[name] = currentVal; } self._cache[name] = newVal; self.trigger('change:' + name, self, self._cache[name]); @@ -557,6 +561,7 @@ function createDerivedProperty(modelProto, name, definition) { var def = modelProto._derived[name] = { fn: _.isFunction(definition) ? definition : definition.fn, cache: (definition.cache !== false), + type: definition.type || 'any', depList: definition.deps || [] }; @@ -769,4 +774,4 @@ function extend(protoProps) { Base.extend = extend; // Our main exports -module.exports = Base; +module.exports = Base; \ No newline at end of file From 58814f90bea346f2303bdeae8ae59fde4be165f4 Mon Sep 17 00:00:00 2001 From: Mat Tyndall Date: Fri, 5 Dec 2014 23:10:14 -0800 Subject: [PATCH 2/3] refactored get for derived props to obey dataType, added init option for derived, added tests --- ampersand-state.js | 37 +++++++++----- test/basics.js | 43 ++++++++++++++++ test/full.js | 124 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 13 deletions(-) diff --git a/ampersand-state.js b/ampersand-state.js index cc59b87..c96a502 100644 --- a/ampersand-state.js +++ b/ampersand-state.js @@ -382,16 +382,14 @@ _.extend(Base.prototype, BBEvents, { _initDerived: function () { var self = this; - _.each(this._derived, function (value, name) { - var def = self._derived[name]; + _.each(this._derived, function (def, name) { def.deps = def.depList; - var update = function (options) { - options = options || {}; + var isEqual = self._getCompareForType(def.type); + var update = function () { var newVal = def.fn.call(self); - var isEqual = self._getCompareForType(def.type); var currentVal = self._cache[name]; var hasChanged = !isEqual(currentVal, newVal, name); @@ -407,6 +405,12 @@ _.extend(Base.prototype, BBEvents, { def.deps.forEach(function (propString) { self._keyTree.add(propString, update); }); + + // init to set any listeners + if (def.init) { + self._getDerivedProperty(name); + isEqual(null, self._cache[name], name); + } }); this.on('all', function (eventName) { @@ -418,16 +422,17 @@ _.extend(Base.prototype, BBEvents, { }, this); }, - _getDerivedProperty: function (name, flushCache) { + _getDerivedProperty: function (name) { + var def = this._derived[name]; // is this a derived property that is cached - if (this._derived[name].cache) { - //set if this is the first time, or flushCache is set - if (flushCache || !this._cache.hasOwnProperty(name)) { - this._cache[name] = this._derived[name].fn.apply(this); + if (def.cache) { + // set if this is the first time + if (!this._cache.hasOwnProperty(name)) { + this._cache[name] = def.fn.apply(this); } return this._cache[name]; } else { - return this._derived[name].fn.apply(this); + return def.fn.apply(this); } }, @@ -561,7 +566,8 @@ function createDerivedProperty(modelProto, name, definition) { var def = modelProto._derived[name] = { fn: _.isFunction(definition) ? definition : definition.fn, cache: (definition.cache !== false), - type: definition.type || 'any', + type: definition.type, + init: definition.init, depList: definition.deps || [] }; @@ -573,7 +579,12 @@ function createDerivedProperty(modelProto, name, definition) { // defined a top-level getter for derived names Object.defineProperty(modelProto, name, { get: function () { - return this._getDerivedProperty(name); + var result = this._getDerivedProperty(name); + var typeDef = this._dataTypes[def.type]; + if (typeDef && typeDef.get) { + result = typeDef.get(result); + } + return result; }, set: function () { throw new TypeError('"' + name + '" is a derived property, it can\'t be set directly.'); diff --git a/test/basics.js b/test/basics.js index e31dbd3..d7f0b3b 100644 --- a/test/basics.js +++ b/test/basics.js @@ -156,6 +156,49 @@ test('uncached derived properties always fire events on dependency change', func person.name = 'different'; }); +test('derived properties with type: `state` will be evented', function (t) { + var ran = 0; + var coolCheck; + var AwesomePerson = Person.extend({ + props: { + awesomeness: 'number', + coolness: 'number' + } + }); + var friend = new AwesomePerson({name: 'mat', awesomeness: 1, coolness: 1}); + var NewPerson = Person.extend({ + props: { + friendName: 'string' + }, + derived: { + friend: { + deps: ['friendName'], + type: 'state', + init: true, + fn: function () { + ran++; + return this.friendName === 'mat' ? friend : null; + } + } + } + }); + var person = new NewPerson({name: 'henrik', friendName: 'mat'}); + person.on('change:friend.coolness', function (model, value) { + t.equal(value, 3, "listens to changes on derived property attribute at init"); + coolCheck = true; + }); + person.on('change:friend.awesomeness', function (model, value) { + t.equal(value, 7, "Fires update for derived property attribute"); + t.end(); + }); + t.equal(ran, 1); + friend.coolness = 3; + t.ok(coolCheck, 'coolCheck'); + t.equal(person.friend.awesomeness, 1); + person.friend.awesomeness = 7; + t.equal(ran, 1); +}); + test('everything should work with a property called `type`. Issue #6.', function (t) { var Model = State.extend({ props: { diff --git a/test/full.js b/test/full.js index 43e8fa9..2d6e959 100644 --- a/test/full.js +++ b/test/full.js @@ -546,6 +546,130 @@ test('derived properties triggered with multiple instances', function (t) { t.end(); }); +test('derived properties with `type` will use `dataType.get` and `dataType.compare`', function (t) { + var friendRan = 0; + var crazyRan = 0; + var compareCheck, coolCheck, changeCheck, thingCheck, weirdCheck; + + var fooFriends = {}; + + var Foo = State.extend({ + extraProperties: 'allow', + props: { + name: ['string', true], + friendName: 'string', + crazy: 'boolean', + cool: 'boolean' + }, + derived: { + friend: { + deps: ['friendName'], + type: 'state', + init: true, + fn: function () { + friendRan++; + return fooFriends[this.friendName]; + } + }, + crazyFriend: { + deps: ['friend', 'friend.name', 'friend.crazy'], + type: 'crazyType', + fn: function () { + crazyRan++; + if (!this.friend) return ''; + return this.friend.name + ' is ' + (this.friend.crazy ? '' : 'not '); + } + } + }, + dataTypes: { + crazyType: { + compare: function (oldVal, newVal, name) { + compareCheck = true; + return false; + }, + set: function (newVal) { + return { + val: newVal, + type: 'crazyType' + }; + }, + get: function (val) { + return val + 'crazy!'; + } + } + } + }); + + fooFriends.mat = new Foo({ + name: 'mat', + cool: false, + weird: false + }); + t.equal(friendRan, 1); + t.equal(crazyRan, 0); + fooFriends.cat = new Foo({ + name: 'cat', + cool: false, + weird: false + }); + t.equal(friendRan, 2); + + var foo = new Foo({ + name: 'abe', + friendName: 'mat' + }); + t.equal(friendRan, 3); + + foo.on('change:friend.weird', function (model, value) { + t.ok(value, "Fires update for derived property attribute"); + weirdCheck = true; + }); + foo.on('change:friend.cool', function (model, value) { + t.ok(value, "Fires compare for derived state on init"); + coolCheck = true; + }); + + foo.friend.cool = true; + foo.friend.weird = true; + t.ok(foo.friend.weird); + t.equal(friendRan, 3); + + var bar = new Foo({ + name: 'bob', + friendName: 'nobody' + }); + + bar.on('change:friend', function (model, value) { + t.ok(value, "Fires on change"); + changeCheck = true; + }); + bar.on('change:friend:thing', function (model, value) { + t.equal(value, 'thing', "Fires on change of derived child ad hoc properties"); + thingCheck = true; + }); + + bar.friendName = 'cat'; + t.equal(friendRan, 5); + t.equal(bar.friend.name, 'cat'); + bar.friendName = 'mat'; + t.equal(bar.friend.name, 'mat'); + t.equal(friendRan, 6); + + bar.friend.thing = 'thing'; + + t.equal(crazyRan, 6); + t.equal(bar.crazyFriend, 'mat is not crazy!', 'derived properties with dataType should use dataType.get'); + bar.friend.crazy = true; + t.equal(bar.crazyFriend, 'mat is crazy!', 'derived result for dataType should change'); + t.equal(crazyRan, 8); + + t.ok(compareCheck, 'passed compareCheck'); + t.ok(changeCheck, 'passed changeCheck'); + t.ok(coolCheck, 'passed coolCheck'); + t.ok(weirdCheck, 'passed weirdCheck'); + t.end(); +}); + test('Calling `previous` during change of derived cached property should work', function (t) { var foo = new Foo({firstName: 'Henrik', lastName: 'Joreteg'}); var ran = false; From 762a88a1f9d82db1bfe671d788a95fb8e77b2ea8 Mon Sep 17 00:00:00 2001 From: Mat Tyndall Date: Wed, 10 Dec 2014 15:43:33 -0800 Subject: [PATCH 3/3] support and tests for derived default and all dataType functions --- ampersand-state.js | 35 +++++++++++++++++++++++++---------- test/full.js | 29 ++++++++++++++++++----------- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/ampersand-state.js b/ampersand-state.js index c96a502..3d5bae9 100644 --- a/ampersand-state.js +++ b/ampersand-state.js @@ -385,11 +385,11 @@ _.extend(Base.prototype, BBEvents, { _.each(this._derived, function (def, name) { def.deps = def.depList; - var isEqual = self._getCompareForType(def.type); - var update = function () { var newVal = def.fn.call(self); + var isEqual = self._getCompareForType(def.type); + var dataType = def.type && self._dataTypes[def.type]; var currentVal = self._cache[name]; var hasChanged = !isEqual(currentVal, newVal, name); @@ -397,8 +397,20 @@ _.extend(Base.prototype, BBEvents, { if (def.cache) { self._previousAttributes[name] = currentVal; } + // cast newVal if there is a type set + if (dataType && dataType.set) { + newVal = dataType.set(newVal).val; + } self._cache[name] = newVal; - self.trigger('change:' + name, self, self._cache[name]); + + // check for default or get for the change event value + if (typeof newVal === 'undefined') { + newVal = _.result(def, 'default'); + } + else if (dataType && dataType.get) { + newVal = dataType.get(newVal); + } + self.trigger('change:' + name, self, newVal); } }; @@ -407,10 +419,7 @@ _.extend(Base.prototype, BBEvents, { }); // init to set any listeners - if (def.init) { - self._getDerivedProperty(name); - isEqual(null, self._cache[name], name); - } + if (def.init) update(); }); this.on('all', function (eventName) { @@ -568,9 +577,12 @@ function createDerivedProperty(modelProto, name, definition) { cache: (definition.cache !== false), type: definition.type, init: definition.init, + default: definition.default, depList: definition.deps || [] }; + if (def.type && _.isUndefined(def.default)) + def.default = modelProto._getDefaultForType(def.type); // add to our shared dependency list _.each(def.depList, function (dep) { modelProto._deps[dep] = _(modelProto._deps[dep] || []).union([name]); @@ -581,10 +593,13 @@ function createDerivedProperty(modelProto, name, definition) { get: function () { var result = this._getDerivedProperty(name); var typeDef = this._dataTypes[def.type]; - if (typeDef && typeDef.get) { - result = typeDef.get(result); + if (typeof result !== 'undefined') { + if (typeDef && typeDef.get) { + result = typeDef.get(result); + } + return result; } - return result; + return _.result(def, 'default'); }, set: function () { throw new TypeError('"' + name + '" is a derived property, it can\'t be set directly.'); diff --git a/test/full.js b/test/full.js index 2d6e959..fcdf24e 100644 --- a/test/full.js +++ b/test/full.js @@ -546,7 +546,7 @@ test('derived properties triggered with multiple instances', function (t) { t.end(); }); -test('derived properties with `type` will use `dataType.get` and `dataType.compare`', function (t) { +test('derived properties with `type` will use dataType functions', function (t) { var friendRan = 0; var crazyRan = 0; var compareCheck, coolCheck, changeCheck, thingCheck, weirdCheck; @@ -566,6 +566,7 @@ test('derived properties with `type` will use `dataType.get` and `dataType.compa deps: ['friendName'], type: 'state', init: true, + default: 'no friend', fn: function () { friendRan++; return fooFriends[this.friendName]; @@ -576,8 +577,9 @@ test('derived properties with `type` will use `dataType.get` and `dataType.compa type: 'crazyType', fn: function () { crazyRan++; - if (!this.friend) return ''; - return this.friend.name + ' is ' + (this.friend.crazy ? '' : 'not '); + if (this.friend.name) { + return this.friend.name + ' is ' + (this.friend.crazy ? '' : 'not '); + } } } }, @@ -595,6 +597,9 @@ test('derived properties with `type` will use `dataType.get` and `dataType.compa }, get: function (val) { return val + 'crazy!'; + }, + default: function () { + return 'crazy!'; } } } @@ -606,19 +611,21 @@ test('derived properties with `type` will use `dataType.get` and `dataType.compa weird: false }); t.equal(friendRan, 1); - t.equal(crazyRan, 0); + t.equal(fooFriends.mat.friend, 'no friend', 'apply derived default when undefined'); + t.equal(fooFriends.mat.crazyFriend, 'crazy!', 'apply dataType default when undefined'); + t.equal(crazyRan, 1); fooFriends.cat = new Foo({ name: 'cat', cool: false, weird: false }); - t.equal(friendRan, 2); + t.equal(friendRan, 3); var foo = new Foo({ name: 'abe', friendName: 'mat' }); - t.equal(friendRan, 3); + t.equal(friendRan, 4); foo.on('change:friend.weird', function (model, value) { t.ok(value, "Fires update for derived property attribute"); @@ -632,7 +639,7 @@ test('derived properties with `type` will use `dataType.get` and `dataType.compa foo.friend.cool = true; foo.friend.weird = true; t.ok(foo.friend.weird); - t.equal(friendRan, 3); + t.equal(friendRan, 4); var bar = new Foo({ name: 'bob', @@ -649,19 +656,19 @@ test('derived properties with `type` will use `dataType.get` and `dataType.compa }); bar.friendName = 'cat'; - t.equal(friendRan, 5); + t.equal(friendRan, 6); t.equal(bar.friend.name, 'cat'); bar.friendName = 'mat'; t.equal(bar.friend.name, 'mat'); - t.equal(friendRan, 6); + t.equal(friendRan, 7); bar.friend.thing = 'thing'; - t.equal(crazyRan, 6); + t.equal(crazyRan, 7); t.equal(bar.crazyFriend, 'mat is not crazy!', 'derived properties with dataType should use dataType.get'); bar.friend.crazy = true; t.equal(bar.crazyFriend, 'mat is crazy!', 'derived result for dataType should change'); - t.equal(crazyRan, 8); + t.equal(crazyRan, 9); t.ok(compareCheck, 'passed compareCheck'); t.ok(changeCheck, 'passed changeCheck');