Skip to content

Commit

Permalink
saving changes for now, see failing test for current status
Browse files Browse the repository at this point in the history
  • Loading branch information
HenrikJoreteg committed May 29, 2014
1 parent c9d0adb commit 43123b6
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 84 deletions.
130 changes: 116 additions & 14 deletions ampersand-state.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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:/;


Expand Down Expand Up @@ -147,20 +147,17 @@ _.extend(Base.prototype, BBEvents, {
throw new TypeError('Property \'' + attr + '\' must be one of values: ' + def.values.join(', '));
}

hasChanged = !isEqual(currentVal, newVal);
hasChanged = !isEqual(currentVal, newVal, attr);

// enforce `setOnce` for properties if set
if (def.setOnce && currentVal !== undefined && hasChanged) {
throw new TypeError('Property \'' + key + '\' can only be set once.');
}

// 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;
} else {
delete self._changed[attr];
Expand Down Expand Up @@ -290,7 +287,7 @@ _.extend(Base.prototype, BBEvents, {
// Determine which comparison algorithm to use for comparing a property
_getCompareForType: function (type) {
var dataType = this._dataTypes[type];
if (dataType && dataType.compare) return dataType.compare;
if (dataType && dataType.compare) return _.bind(dataType.compare, this);
return _.isEqual;
},

Expand Down Expand Up @@ -356,7 +353,6 @@ _.extend(Base.prototype, BBEvents, {
}
self._cache[name] = newVal;
self.trigger('change:' + name, self, self._cache[name]);
if (options.triggerChange) self.trigger('change');
}
};

Expand Down Expand Up @@ -399,14 +395,20 @@ _.extend(Base.prototype, BBEvents, {
if (!this._children) return;
for (child in this._children) {
this[child] = new this._children[child]({}, {parent: this});
this.listenTo(this[child], 'all', function (name, model, newValue) {
if (changeRE.test(name)) {
this.trigger('change:' + child + '.' + name.split(':')[1], model, newValue);
}
});
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
Expand Down Expand Up @@ -511,6 +513,106 @@ function createDerivedProperty(modelProto, name, definition) {
});
}

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) {
console.log('starting to listen', attributeName);
this.listenTo(newVal, 'all', this._getEventBubblingHandler(attributeName));
}
}

return isSame;
}
}
};

// the extend method used to extend prototypes, maintain inheritance chains for instanceof
// and allow for additions to the model definitions.
function extend(protoProps) {
Expand Down
69 changes: 0 additions & 69 deletions dataTypes.js

This file was deleted.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"dependencies": {
"array-next": "~0.0.1",
"backbone-events-standalone": "0.2.1",
"underscore": "^1.6.0"
"underscore": "^1.6.0",
"key-tree-store": "~0.1.0"
},
"devDependencies": {
"ampersand-collection": "^1.2.0",
Expand Down
102 changes: 102 additions & 0 deletions test/full.js
Original file line number Diff line number Diff line change
Expand Up @@ -1029,3 +1029,105 @@ test('Should be able to declare derived properties that have nested deps', funct

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');

t.end();
});

0 comments on commit 43123b6

Please sign in to comment.