Socket
Socket
Sign inDemoInstall

ampersand-state

Package Overview
Dependencies
Maintainers
1
Versions
65
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ampersand-state - npm Package Compare versions

Comparing version 4.0.1 to 4.1.0

252

ampersand-state.js
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 @@ });

5

package.json
{
"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",

@@ -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({

@@ -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';
});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc