ampersand-state
Advanced tools
Comparing version 4.0.1 to 4.1.0
var _ = require('underscore'); | ||
var BBEvents = require('backbone-events-standalone'); | ||
var KeyTree = require('key-tree-store'); | ||
var arrayNext = require('array-next'); | ||
var dataTypes = require('./dataTypes'); | ||
var changeRE = /^change:/; | ||
@@ -9,10 +10,15 @@ | ||
options || (options = {}); | ||
this._values = {}; | ||
this._definition = {}; | ||
if (options.parse) attrs = this.parse(attrs, options); | ||
this._values = {}; | ||
if (options.parent) this.parent = options.parent; | ||
this._keyTree = new KeyTree(); | ||
this._initCollections(); | ||
this._initChildren(); | ||
this._cache = {}; | ||
this._previousAttributes = {}; | ||
this._events = {}; | ||
if (attrs) this.set(attrs, _.extend({silent: true, initial: true}, options)); | ||
this._changed = {}; | ||
if (attrs) this.set(attrs, _.extend({silent: true, initial: true}, options)); | ||
if (this._derived) this._initDerived(); | ||
if (options.init !== false) this.initialize.apply(this, arguments); | ||
@@ -40,3 +46,10 @@ } | ||
serialize: function () { | ||
return this.getAttributes({props: true}, true); | ||
var res = this.getAttributes({props: true}, true); | ||
_.each(this._children, function (value, key) { | ||
res[key] = this[key].serialize(); | ||
}, this); | ||
_.each(this._collections, function (value, key) { | ||
res[key] = this[key].serialize(); | ||
}, this); | ||
return res; | ||
}, | ||
@@ -88,7 +101,12 @@ | ||
currentVal = this._values[attr]; | ||
def = this._definition[attr]; | ||
def = this._getDefinition(attr); | ||
if (!def) { | ||
if (extraProperties === 'ignore') { | ||
// if this is a child model or collection | ||
if (this._children[attr] || this._collections[attr]) { | ||
this[attr].set(newVal, options); | ||
continue; | ||
} else if (extraProperties === 'ignore') { | ||
continue; | ||
} else if (extraProperties === 'reject') { | ||
@@ -136,3 +154,3 @@ throw new TypeError('No "' + attr + '" property defined on ' + (this.type || 'this') + ' model and allowOtherProperties not set.'); | ||
hasChanged = !isEqual(currentVal, newVal); | ||
hasChanged = !isEqual(currentVal, newVal, attr); | ||
@@ -144,9 +162,6 @@ // enforce `setOnce` for properties if set | ||
// push to changes array if different | ||
// keep track of changed attributes | ||
// and push to changes array | ||
if (hasChanged) { | ||
changes.push({prev: currentVal, val: newVal, key: attr}); | ||
} | ||
// keep track of changed attributes | ||
if (!isEqual(previous[attr], newVal)) { | ||
self._changed[attr] = newVal; | ||
@@ -168,28 +183,9 @@ } else { | ||
function gatherTriggers(key) { | ||
triggers.push(key); | ||
_.each((self._deps[key] || []), function (derTrigger) { | ||
gatherTriggers(derTrigger); | ||
if (!silent && changes.length) self._pending = true; | ||
if (!silent) { | ||
_.each(changes, function (change) { | ||
self.trigger('change:' + change.key, self, change.val); | ||
}); | ||
} | ||
if (!silent && changes.length) self._pending = true; | ||
_.each(changes, function (change) { | ||
gatherTriggers(change.key); | ||
}); | ||
_.each(_.uniq(triggers), function (key) { | ||
var derived = self._derived[key]; | ||
if (derived && derived.cache && !initial) { | ||
var oldDerived = self._cache[key]; | ||
var newDerived = self._getDerivedProperty(key, true); | ||
if (!_.isEqual(oldDerived, newDerived)) { | ||
self._previousAttributes[key] = oldDerived; | ||
if (!silent) self.trigger('change:' + key, self, newDerived); | ||
} | ||
} else { | ||
if (!silent) self.trigger('change:' + key, self, self[key]); | ||
} | ||
}); | ||
// You might be wondering why there's a `while` loop here. Changes can | ||
@@ -216,3 +212,3 @@ // be recursively nested within `"change"` events. | ||
toggle: function (property) { | ||
var def = this._definition[property]; | ||
var def = this._getDefinition(property); | ||
if (def.type === 'boolean') { | ||
@@ -256,3 +252,3 @@ // if it's a bool, just flip it | ||
for (var attr in diff) { | ||
def = this._definition[attr]; | ||
def = this._getDefinition(attr); | ||
isEqual = this._getCompareForType(def && def.type); | ||
@@ -270,3 +266,3 @@ if (isEqual(old[attr], (val = diff[attr]))) continue; | ||
unset: function (attr, options) { | ||
var def = this._definition[attr]; | ||
var def = this._getDefinition(attr); | ||
var type = def.type; | ||
@@ -304,3 +300,3 @@ var val; | ||
var dataType = this._dataTypes[type]; | ||
if (dataType && dataType.compare) return dataType.compare; | ||
if (dataType && dataType.compare) return _.bind(dataType.compare, this); | ||
return _.isEqual; | ||
@@ -339,4 +335,4 @@ }, | ||
var val, item, def; | ||
for (item in this._definition) { | ||
def = this._definition[item]; | ||
for (item in this._getDefinition()) { | ||
def = this._getDefinition(item); | ||
if ((options.session && def.session) || (options.props && !def.session)) { | ||
@@ -354,2 +350,44 @@ val = (raw) ? this._values[item] : this[item]; | ||
_initDerived: function () { | ||
var self = this; | ||
_.each(this._derived, function (value, name) { | ||
var def = self._derived[name]; | ||
def.deps = def.depList; | ||
var update = function (options) { | ||
options = options || {}; | ||
var newVal = def.fn.call(self); | ||
if (self._cache[name] !== newVal || !def.cache) { | ||
if (def.cache) { | ||
self._previousAttributes[name] = self._cache[name]; | ||
} | ||
self._cache[name] = newVal; | ||
self.trigger('change:' + name, self, self._cache[name]); | ||
} | ||
}; | ||
def.deps.forEach(function (propString) { | ||
self._keyTree.add(propString, update); | ||
}); | ||
}); | ||
this.on('all', function (eventName) { | ||
if (changeRE.test(eventName)) { | ||
self._keyTree.get(eventName.split(':')[1]).forEach(function (fn) { | ||
fn(); | ||
}); | ||
} | ||
}, this); | ||
}, | ||
_getDefinition: function (attr) { | ||
if (attr) { | ||
return this._definition[attr] || this.constructor.prototype._definition[attr]; | ||
} | ||
return _.extend({}, this._definition, this.constructor.prototype._definition); | ||
}, | ||
_getDerivedProperty: function (name, flushCache) { | ||
@@ -372,12 +410,30 @@ // is this a derived property that is cached | ||
for (coll in this._collections) { | ||
this[coll] = new this._collections[coll](); | ||
this[coll].parent = this; | ||
this[coll] = new this._collections[coll]([], {parent: this}); | ||
} | ||
}, | ||
_initChildren: function () { | ||
var child; | ||
if (!this._children) return; | ||
for (child in this._children) { | ||
this[child] = new this._children[child]({}, {parent: this}); | ||
this.listenTo(this[child], 'all', this._getEventBubblingHandler(child)); | ||
} | ||
}, | ||
// Returns a bound handler for doing event bubbling while | ||
// adding a name to the change string. | ||
_getEventBubblingHandler: function (propertyName) { | ||
return _.bind(function (name, model, newValue) { | ||
if (changeRE.test(name)) { | ||
this.trigger('change:' + propertyName + '.' + name.split(':')[1], model, newValue); | ||
} | ||
}, this); | ||
}, | ||
// Check that all required attributes are present | ||
_verifyRequired: function () { | ||
var attrs = this.attributes; // should include session | ||
for (var def in this._definition) { | ||
if (this._definition[def].required && typeof attrs[def] === 'undefined') { | ||
for (var def in this._getDefinition()) { | ||
if (this._getDefinition(def).required && typeof attrs[def] === 'undefined') { | ||
return false; | ||
@@ -482,2 +538,101 @@ } | ||
var dataTypes = { | ||
string: { | ||
default: function () { | ||
return ''; | ||
} | ||
}, | ||
date: { | ||
set: function (newVal) { | ||
var newType; | ||
if (!_.isDate(newVal)) { | ||
try { | ||
newVal = new Date(parseInt(newVal, 10)); | ||
if (!_.isDate(newVal)) throw TypeError; | ||
newVal = newVal.valueOf(); | ||
if (_.isNaN(newVal)) throw TypeError; | ||
newType = 'date'; | ||
} catch (e) { | ||
newType = typeof newVal; | ||
} | ||
} else { | ||
newType = 'date'; | ||
newVal = newVal.valueOf(); | ||
} | ||
return { | ||
val: newVal, | ||
type: newType | ||
}; | ||
}, | ||
get: function (val) { | ||
return new Date(val); | ||
}, | ||
default: function () { | ||
return new Date(); | ||
} | ||
}, | ||
array: { | ||
set: function (newVal) { | ||
return { | ||
val: newVal, | ||
type: _.isArray(newVal) ? 'array' : typeof newVal | ||
}; | ||
}, | ||
default: function () { | ||
return []; | ||
} | ||
}, | ||
object: { | ||
set: function (newVal) { | ||
var newType = typeof newVal; | ||
// we have to have a way of supporting "missing" objects. | ||
// Null is an object, but setting a value to undefined | ||
// should work too, IMO. We just override it, in that case. | ||
if (newType !== 'object' && _.isUndefined(newVal)) { | ||
newVal = null; | ||
newType = 'object'; | ||
} | ||
return { | ||
val: newVal, | ||
type: newType | ||
}; | ||
}, | ||
default: function () { | ||
return {}; | ||
} | ||
}, | ||
// the `state` data type is a bit special in that setting it should | ||
// also bubble events | ||
state: { | ||
set: function (newVal) { | ||
var isInstance = newVal instanceof Base; | ||
if (isInstance) { | ||
return { | ||
val: newVal, | ||
type: 'state' | ||
}; | ||
} else { | ||
return { | ||
val: newVal, | ||
type: typeof newVal | ||
}; | ||
} | ||
}, | ||
compare: function (currentVal, newVal, attributeName) { | ||
var isSame = currentVal === newVal; | ||
// if this has changed we want to also handle | ||
// event propagation | ||
if (!isSame) { | ||
this.stopListening(currentVal); | ||
if (newVal != null) { | ||
this.listenTo(newVal, 'all', this._getEventBubblingHandler(attributeName)); | ||
} | ||
} | ||
return isSame; | ||
} | ||
} | ||
}; | ||
// the extend method used to extend prototypes, maintain inheritance chains for instanceof | ||
@@ -516,2 +671,3 @@ // and allow for additions to the model definitions. | ||
child.prototype._collections = _.extend({}, parent.prototype._collections); | ||
child.prototype._children = _.extend({}, parent.prototype._children); | ||
child.prototype._dataTypes = _.extend({}, parent.prototype._dataTypes || dataTypes); | ||
@@ -552,2 +708,8 @@ | ||
} | ||
if (def.children) { | ||
_.each(def.children, function (constructor, name) { | ||
child.prototype._children[name] = constructor; | ||
}); | ||
delete def.children; | ||
} | ||
_.extend(child.prototype, def); | ||
@@ -554,0 +716,0 @@ }); |
{ | ||
"name": "ampersand-state", | ||
"description": "An observable, extensible state object with derived watchable properties.", | ||
"version": "4.0.1", | ||
"version": "4.1.0", | ||
"author": "Henrik Joreteg <henrik@andyet.net>", | ||
@@ -12,6 +12,7 @@ "bugs": { | ||
"backbone-events-standalone": "0.2.1", | ||
"key-tree-store": "~0.1.0", | ||
"underscore": "^1.6.0" | ||
}, | ||
"devDependencies": { | ||
"ampersand-collection": "^1.0.0", | ||
"ampersand-collection": "^1.2.0", | ||
"ampersand-registry": "0.x.x", | ||
@@ -18,0 +19,0 @@ "precommit-hook": "~0.3.10", |
113
README.md
@@ -272,8 +272,113 @@ # ampersand-state | ||
## child models and collections | ||
You can declare children and collections that will get instantiated on init as follows: | ||
```js | ||
var State = require('ampersand-state'); | ||
var Messages = require('./models/messages'); | ||
var ProfileModel = require('./models/profile'); | ||
var Person = State.extend({ | ||
props: { | ||
name: 'string' | ||
}, | ||
collections: { | ||
// `Messages` here is a collection | ||
messages: Messages | ||
}, | ||
children: { | ||
// `ProfileModel` is another ampersand-state constructor | ||
profile: ProfileModel | ||
} | ||
}); | ||
// When we instantiate an instance of a Person | ||
// the Messages collection and ProfileModels | ||
// are instantiated as well | ||
var person = new Person(); | ||
// so meetings exists as an empty collection | ||
person.meetings instanceof Meetings; // true | ||
// and profile exists as an empty `ProfileModel` | ||
person.profile instanceof ProfileModel; // true | ||
// This also provides some additional capabilities | ||
// when we instantiate a state object with some | ||
// data it will apply them to the collections and child | ||
// models as you might expect: | ||
var otherPerson = new Person({ | ||
messages: [ | ||
{from: 'someone', message: 'hi'}, | ||
{from: 'someoneElse', message: 'yo!'}, | ||
], | ||
profile: { | ||
name: 'Joe', | ||
hairColor: 'black' | ||
} | ||
}); | ||
// now messages would have a length | ||
otherPerson.messages.length === 2; // true | ||
// and the profile state object would be | ||
// populated | ||
otherPerson.profile.name === 'Joe'; // true | ||
// The same works for `set`, it will apply it | ||
// to children as well. | ||
otherPerson.set({profile: {name: 'Mary'}}); | ||
// Since this a state object it triggers a `change:name` on | ||
// the `profile` object. | ||
// In addition, since it's a child that event propagates | ||
// up. More on that below. | ||
``` | ||
## Event bubbling, derived properties based on children | ||
Say you want a simple way to listen for any changes that are represented in a tempalate. | ||
Let's say you've got a `person` state object with a `profile` child. You want an easy way to listen for changes to either the base `person` object or the `profile`. In fact, you want to listen to anything related to the person object. | ||
Rather than having to worry about watching the right thing, we do exactly what the browser does to solve this problem: we bubble up the events up the chain. | ||
Now we can listen for deeply nested changes to properties. | ||
And we can declare derived properties that depend on children. For example: | ||
```js | ||
var Person = State.extend({ | ||
children: { | ||
profile: Profile | ||
}, | ||
derived: { | ||
childsName: { | ||
// now we can declare a child as a | ||
// dependency | ||
deps: ['profile.name'], | ||
fn: function () { | ||
return 'my child\'s name is ' + this.profile.name; | ||
} | ||
} | ||
} | ||
}); | ||
var me = new Person(); | ||
// we can listen for changes to the derived property | ||
me.on('change:childsName', function (model, newValue) { | ||
console.log(newValue); // logs out `my child's name is henrik` | ||
}); | ||
// so when a property of a child is changed the callback | ||
// above will be fired (if the resulting derived property is different) | ||
me.profile.name = 'henrik'; | ||
``` | ||
## Changelog | ||
- 0.1.0 - lots of cleanup, grabbing tests from human-model, now maintains the prototype chain so `instanceof` checks pass no matter how many times it's been extended. | ||
- 0.0.2 - improved doc | ||
- 0.0.1 - initial publish | ||
<!-- starthide --> | ||
@@ -280,0 +385,0 @@ ## Credits |
@@ -28,2 +28,9 @@ /*jshint expr: true*/ | ||
test('after initialized change should be empty until a set op', function (t) { | ||
var person = new Person({name: 'phil'}); | ||
t.deepEqual(person._changed, {}); | ||
t.notOk(person.changedAttributes()); | ||
t.end(); | ||
}); | ||
test('extended object maintains existing props', function (t) { | ||
@@ -30,0 +37,0 @@ var AwesomePerson = Person.extend({ |
373
test/full.js
@@ -13,2 +13,6 @@ var tape = require('tape'); | ||
}; | ||
test.only = function () { | ||
reset(); | ||
tape.only.apply(tape, arguments); | ||
}; | ||
@@ -79,3 +83,2 @@ function reset() { | ||
test('should get the derived value', function (t) { | ||
@@ -208,3 +211,3 @@ var foo = new Foo({ | ||
test('Setting other properties is ok if allowOtherProperties is true', function (t) { | ||
test('Setting other properties is ok if extraProperties = "allow"', function (t) { | ||
var foo = new Foo(); | ||
@@ -219,2 +222,32 @@ foo.extraProperties = 'allow'; | ||
test('#11 - multiple instances of the same state class should be able to use extraProperties = "allow" as expected', function (t) { | ||
var Foo = State.extend({ | ||
extraProperties: 'allow' | ||
}); | ||
var one = new Foo({ a: 'one.a', b: 'one.b' }); | ||
var two = new Foo({ a: 'two.a', b: 'two.b', c: 'two.c' }); | ||
t.equal(one.a, 'one.a'); | ||
t.equal(one.b, 'one.b'); | ||
t.equal(two.a, 'two.a'); | ||
t.equal(two.b, 'two.b'); | ||
t.equal(two.c, 'two.c'); | ||
t.end(); | ||
}); | ||
test('extraProperties = "allow" properties should be defined entirely on the instance not the prototype', function (t) { | ||
var Foo = State.extend({ | ||
extraProperties: 'allow' | ||
}); | ||
var one = new Foo({ a: 'one.a', b: 'one.b' }); | ||
var two = new Foo(); | ||
t.deepEqual(two._definition, {}); | ||
t.end(); | ||
}); | ||
test('should throw a type error for bad data types', function (t) { | ||
@@ -726,5 +759,3 @@ t.throws(function () { | ||
AwesomeThing.prototype = Object.create(StateObj.prototype, { | ||
constructor: AwesomeThing | ||
}); | ||
AwesomeThing.prototype = Object.create(StateObj.prototype); | ||
@@ -775,3 +806,5 @@ AwesomeThing.prototype.hello = function () { | ||
var StateObj = State.extend({ | ||
id: 'string' | ||
props: { | ||
id: 'string' | ||
} | ||
}); | ||
@@ -786,1 +819,329 @@ var c = new Collection(); | ||
}); | ||
test('children and collections should be instantiated', function (t) { | ||
var GrandChild = State.extend({ | ||
props: { | ||
id: 'string' | ||
}, | ||
collections: { | ||
nicknames: Collection | ||
} | ||
}); | ||
var FirstChild = State.extend({ | ||
props: { | ||
id: 'string' | ||
}, | ||
children: { | ||
grandChild: GrandChild | ||
} | ||
}); | ||
var StateObj = State.extend({ | ||
props: { | ||
id: 'string' | ||
}, | ||
children: { | ||
firstChild: FirstChild | ||
} | ||
}); | ||
var data = { | ||
id: 'child', | ||
firstChild: { | ||
id: 'child', | ||
grandChild: { | ||
id: 'grandChild', | ||
nicknames: [ | ||
{name: 'munchkin'}, | ||
{name: 'kiddo'} | ||
] | ||
} | ||
} | ||
}; | ||
var first = new StateObj(data); | ||
t.ok(first.firstChild, 'child should be initted'); | ||
t.ok(first.firstChild.grandChild, 'grand child should be initted'); | ||
t.equal(first.firstChild.id, 'child'); | ||
t.equal(first.firstChild.grandChild.id, 'grandChild'); | ||
t.ok(first.firstChild.grandChild.nicknames instanceof Collection, 'should be collection'); | ||
t.equal(first.firstChild.grandChild.nicknames.length, 2); | ||
t.deepEqual(first.serialize(), { | ||
id: 'child', | ||
firstChild: { | ||
id: 'child', | ||
grandChild: { | ||
id: 'grandChild', | ||
nicknames: [ | ||
{name: 'munchkin'}, | ||
{name: 'kiddo'} | ||
] | ||
} | ||
} | ||
}); | ||
t.equal(JSON.stringify(first), JSON.stringify({ | ||
id: 'child', | ||
firstChild: { | ||
id: 'child', | ||
grandChild: { | ||
id: 'grandChild', | ||
nicknames: [ | ||
{name: 'munchkin'}, | ||
{name: 'kiddo'} | ||
] | ||
} | ||
} | ||
}), 'should be able to pass whole object to JSON.stringify()'); | ||
// using `set` should still apply to children | ||
first.set({ | ||
firstChild: { | ||
id: 'firstChild', | ||
grandChild: { | ||
nicknames: [{name: 'runt'}] | ||
} | ||
} | ||
}); | ||
t.ok(first.firstChild instanceof FirstChild, 'should still be instanceof'); | ||
t.equal(first.firstChild.id, 'firstChild', 'change should have been applied'); | ||
t.equal(first.firstChild.grandChild.nicknames.length, 1, 'collection should have been updated'); | ||
t.end(); | ||
}); | ||
test('listens to child events', function (t) { | ||
var GrandChild = State.extend({ | ||
props: { | ||
id: 'string', | ||
name: 'string' | ||
}, | ||
collections: { | ||
nicknames: Collection | ||
} | ||
}); | ||
var FirstChild = State.extend({ | ||
props: { | ||
id: 'string', | ||
name: 'string' | ||
}, | ||
children: { | ||
grandChild: GrandChild | ||
} | ||
}); | ||
var StateObj = State.extend({ | ||
props: { | ||
id: 'string', | ||
name: 'string' | ||
}, | ||
children: { | ||
firstChild: FirstChild | ||
} | ||
}); | ||
var first = new StateObj({ | ||
id: 'child', | ||
name: 'first-name', | ||
firstChild: { | ||
id: 'child', | ||
name: 'first-child-name', | ||
grandChild: { | ||
id: 'grandChild', | ||
name: 'Henrik', | ||
nicknames: [ | ||
{name: 'munchkin'}, | ||
{name: 'kiddo'} | ||
] | ||
} | ||
} | ||
}); | ||
//Change property | ||
first.once('change:name', function (model, newVal) { | ||
t.equal(newVal, 'new-first-name'); | ||
}); | ||
first.name = 'new-first-name'; | ||
t.equal(first.name, 'new-first-name'); | ||
//Change child property | ||
first.once('change:firstChild.name', function (model, newVal) { | ||
t.equal(newVal, 'new-first-child-name'); | ||
}); | ||
first.firstChild.name = 'new-first-child-name'; | ||
t.equal(first.firstChild.name, 'new-first-child-name'); | ||
//Change grand child property | ||
first.on('change:firstChild.grandChild.name', function (unsure, name) { | ||
t.equal(name, "Phil"); | ||
}); | ||
first.firstChild.grandChild.name = 'Phil'; | ||
t.equal(first.firstChild.grandChild.name, 'Phil'); | ||
t.end(); | ||
}); | ||
test('Should be able to declare derived properties that have nested deps', function (t) { | ||
var GrandChild = State.extend({ | ||
props: { | ||
id: 'string', | ||
name: 'string' | ||
} | ||
}); | ||
var FirstChild = State.extend({ | ||
props: { | ||
id: 'string', | ||
name: 'string' | ||
}, | ||
children: { | ||
grandChild: GrandChild | ||
} | ||
}); | ||
var StateObj = State.extend({ | ||
props: { | ||
id: 'string', | ||
name: 'string' | ||
}, | ||
children: { | ||
child: FirstChild | ||
}, | ||
derived: { | ||
relationship: { | ||
deps: ['child.grandChild.name', 'name'], | ||
fn: function () { | ||
return this.name + ' has grandchild ' + (this.child.grandChild.name || ''); | ||
} | ||
} | ||
} | ||
}); | ||
var first = new StateObj({ | ||
name: 'henrik' | ||
}); | ||
t.equal(first.relationship, 'henrik has grandchild ', 'basics properties working'); | ||
first.on('change:relationship', function () { | ||
t.pass('got change event on derived property for child'); | ||
t.end(); | ||
}); | ||
first.child.grandChild.name = 'something'; | ||
}); | ||
test('`state` properties', function (t) { | ||
var Person = State.extend({ | ||
props: { | ||
sub: 'state' | ||
} | ||
}); | ||
var SubState = State.extend({ | ||
props: { | ||
id: 'string' | ||
} | ||
}); | ||
var p = new Person(); | ||
t.plan(4); | ||
t.equal(p.sub, undefined, 'should be undefined to start'); | ||
t.throws(function () { | ||
p.sub = 'something silly'; | ||
}, TypeError, 'Throws type error if not state object'); | ||
p.once('change:sub', function () { | ||
t.pass('fired change for state'); | ||
}); | ||
var sub = new SubState({id: 'hello'}); | ||
p.sub = sub; | ||
p.on('change:sub', function () { | ||
t.fail('shouldnt fire if same instance'); | ||
}); | ||
p.sub = sub; | ||
p.on('change:sub.id', function () { | ||
t.pass('child property event bubbled'); | ||
}); | ||
p.sub.id = 'new'; | ||
// new person | ||
var p2 = new Person(); | ||
var sub1 = new SubState({id: 'first'}); | ||
var sub2 = new SubState({id: 'second'}); | ||
p2.on('change:sub.id', function () { | ||
t.fail('should not bubble on old one'); | ||
}); | ||
p2.sub = sub1; | ||
p2.sub = sub2; | ||
sub1.id = 'something different'; | ||
t.end(); | ||
}); | ||
test('`state` properties should invalidate dependent derived properties when changed', function (t) { | ||
var counter = 0; | ||
var Person = State.extend({ | ||
props: { | ||
sub: 'state' | ||
}, | ||
derived: { | ||
subId: { | ||
deps: ['sub.id'], | ||
fn: function () { | ||
return this.sub && this.sub.id; | ||
} | ||
} | ||
} | ||
}); | ||
var SubState = State.extend({ | ||
props: { | ||
id: 'string' | ||
} | ||
}); | ||
var p = new Person(); | ||
// count each time it's changed | ||
p.on('change:subId', function () { | ||
counter++; | ||
}); | ||
var sub1 = new SubState({id: '1'}); | ||
var sub2 = new SubState({id: '2'}); | ||
t.equal(p.subId, undefined, 'should be undefined to start'); | ||
p.sub = sub1; | ||
t.equal(p.subId, '1', 'should invalidated cache'); | ||
t.equal(counter, 1, 'should fire change callback for derived item'); | ||
p.on('change:sub.id', function (model, newVal) { | ||
t.pass('change event should fire'); | ||
t.equal(model, sub1, 'callback on these should be sub model'); | ||
t.equal(newVal, 'newId', 'should include new val'); | ||
t.end(); | ||
}); | ||
sub1.id = 'newId'; | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
72962
1804
392
4
9
+ Addedkey-tree-store@~0.1.0
+ Addedkey-tree-store@0.1.2(transitive)