diff --git a/ampersand-state.js b/ampersand-state.js index bd221fa..b75cd5c 100644 --- a/ampersand-state.js +++ b/ampersand-state.js @@ -405,27 +405,44 @@ assign(Base.prototype, Events, { _initDerived: function () { var self = this; - forEach(this._derived, function (value, name) { - var def = self._derived[name]; + forEach(this._derived, function (def, name) { def.deps = def.depList; - var update = function (options) { - options = options || {}; - + var update = function () { var newVal = def.fn.call(self); - if (self._cache[name] !== newVal || !def.cache) { + 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); + + if (hasChanged || !def.cache) { if (def.cache) { - self._previousAttributes[name] = self._cache[name]; + 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); } }; def.deps.forEach(function (propString) { self._keyTree.add(propString, update); }); + + // init to set any listeners + if (def.init) update(); }); this.on('all', function (eventName) { @@ -437,16 +454,17 @@ assign(Base.prototype, Events, { }, 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); } }, @@ -582,9 +600,14 @@ function createDerivedProperty(modelProto, name, definition) { var def = modelProto._derived[name] = { fn: isFunction(definition) ? definition : definition.fn, 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 forEach(def.depList, function (dep) { modelProto._deps[dep] = union(modelProto._deps[dep] || [], [name]); @@ -593,7 +616,15 @@ function createDerivedProperty(modelProto, name, definition) { // defined a top-level getter for derived names Object.defineProperty(modelProto, name, { get: function () { - return this._getDerivedProperty(name); + var value = this._getDerivedProperty(name); + var typeDef = this._dataTypes[def.type]; + if (typeof value !== 'undefined') { + if (typeDef && typeDef.get) { + value = typeDef.get(value); + } + return value; + } + return result(def, 'default'); }, 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 1ed2355..fa61bd6 100644 --- a/test/basics.js +++ b/test/basics.js @@ -208,6 +208,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 1dc588e..14f8b17 100644 --- a/test/full.js +++ b/test/full.js @@ -546,6 +546,137 @@ test('derived properties triggered with multiple instances', function (t) { t.end(); }); +test('derived properties with `type` will use dataType functions', 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, + default: 'no friend', + fn: function () { + friendRan++; + return fooFriends[this.friendName]; + } + }, + crazyFriend: { + deps: ['friend', 'friend.name', 'friend.crazy'], + type: 'crazyType', + fn: function () { + crazyRan++; + if (this.friend.name) { + 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!'; + }, + default: function () { + return 'crazy!'; + } + } + } + }); + + fooFriends.mat = new Foo({ + name: 'mat', + cool: false, + weird: false + }); + t.equal(friendRan, 1); + 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, 3); + + var foo = new Foo({ + name: 'abe', + friendName: 'mat' + }); + t.equal(friendRan, 4); + + 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, 4); + + 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, 6); + t.equal(bar.friend.name, 'cat'); + bar.friendName = 'mat'; + t.equal(bar.friend.name, 'mat'); + t.equal(friendRan, 7); + + bar.friend.thing = 'thing'; + + 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, 9); + + 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;