Skip to content

Commit

Permalink
Merge pull request #202 from chesles/datatype-onSet
Browse files Browse the repository at this point in the history
custom datatype onChange function
  • Loading branch information
wraithgar committed Jan 25, 2016
2 parents 37e76cf + 89b01fd commit beebc91
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 14 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ To define a type, you generally will provide an object with 4 member functions (

* `set : function(newVal){}; returns {type : type, val : newVal};`: Called on every set. Should return an object with two members: `val` and `type`. If the `type` value does not equal the name of the dataType you defined, a `TypeError` should be thrown.
* `compare : function(currentVal, newVal, attributeName){}; returns boolean`: Called on every `set`. Should return `true` if `oldVal` and `newVal` are equal. Non-equal values will eventually trigger `change` events, unless the state's `set` (not the dataTypes's!) is called with the option `{silent : true}`.
* `onChange : function (value, previousValue, attributeName){};`: Called after the value changes. Useful for automatically setting up or tearing down listeners on properties.
* `get : function(val){} returns val;`: Overrides the default getter of this type. Useful if you want to make defensive copies. For example, the `date` dataType returns a clone of the internally saved `date` to keep the internal state consistent.
* `default : function(){} returns val;`: Returns the default value for this type.

Expand Down
40 changes: 26 additions & 14 deletions ampersand-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ var Events = require('ampersand-events');
var KeyTree = require('key-tree-store');
var arrayNext = require('array-next');
var changeRE = /^change:/;
var noop = function () {};

function Base(attrs, options) {
options || (options = {});
Expand Down Expand Up @@ -117,7 +118,7 @@ assign(Base.prototype, Events, {
var self = this;
var extraProperties = this.extraProperties;
var changing, changes, newType, newVal, def, cast, err, attr,
attrs, dataType, silent, unset, currentVal, initial, hasChanged, isEqual;
attrs, dataType, silent, unset, currentVal, initial, hasChanged, isEqual, onChange;

// Handle both `"key", value` and `{key: value}` -style arguments.
if (isObject(key) || key === null) {
Expand Down Expand Up @@ -171,6 +172,7 @@ assign(Base.prototype, Events, {
}

isEqual = this._getCompareForType(def.type);
onChange = this._getOnChangeForType(def.type);
dataType = this._dataTypes[def.type];

// check type if we have one
Expand Down Expand Up @@ -223,6 +225,7 @@ assign(Base.prototype, Events, {
if (hasChanged) {
changes.push({prev: currentVal, val: newVal, key: attr});
self._changed[attr] = newVal;
onChange(newVal, currentVal, attr);
} else {
delete self._changed[attr];
}
Expand Down Expand Up @@ -365,6 +368,12 @@ assign(Base.prototype, Events, {
return _isEqual; // if no compare function is defined, use _.isEqual
},

_getOnChangeForType : function(type){
var dataType = this._dataTypes[type];
if (dataType && dataType.onChange) return bind(dataType.onChange, this);
return noop;
},

// Run validation against the next complete set of model attributes,
// returning `true` if all is well. Otherwise, fire an `"invalid"` event.
_validate: function (attrs, options) {
Expand Down Expand Up @@ -594,9 +603,13 @@ function createPropertyDefinition(object, name, desc, isSession) {
}
return value;
}
value = result(def, 'default');
this._values[name] = value;
return value;
var defaultValue = result(def, 'default');
this._values[name] = defaultValue;
if (typeof defaultValue !== 'undefined') {
var onChange = this._getOnChangeForType(def.type);
onChange(defaultValue, value, name);
}
return defaultValue;
}
});

Expand Down Expand Up @@ -716,22 +729,21 @@ var dataTypes = {
};
}
},
compare: function (currentVal, newVal, attributeName) {
var isSame = currentVal === newVal;
compare: function (currentVal, newVal) {
return currentVal === newVal;
},

onChange : function(newVal, previousVal, attributeName){
// if this has changed we want to also handle
// event propagation
if (!isSame) {
if (currentVal) {
this.stopListening(currentVal);
}

if (newVal != null) {
this.listenTo(newVal, 'all', this._getEventBubblingHandler(attributeName));
}
if (previousVal) {
this.stopListening(previousVal);
}

return isSame;
if (newVal != null) {
this.listenTo(newVal, 'all', this._getEventBubblingHandler(attributeName));
}
}
}
};
Expand Down
71 changes: 71 additions & 0 deletions test/full.js
Original file line number Diff line number Diff line change
Expand Up @@ -1815,3 +1815,74 @@ test('toJSON should serialize customType props - issue #197', function(t) {

t.end();
});

test("#112 - should not set up events on child state if setOnce check fails", function(t){
var Person = State.extend({
props : {
birthday : {
type : 'state',
setOnce : true
}
}
});
var Birthday = State.extend({
props : {
day : 'date'
}
});

var p = new Person();
var bday = new Birthday({day : new Date()});
p.once('change:birthday', function() {
t.pass('birthday can change once');
});
p.birthday = bday;
var newBday = new Birthday({day : new Date()});
t.throws(function() {
p.birthday = newBday;
}, TypeError, 'Throws error on change of setOnce');

p.on('change:birthday.day', function() {
t.fail('should not trigger change event on old one');
});

newBday.day = new Date(1);

t.end();
});

test('#112 - onChange should be called for default values', function (t) {
var Person = State.extend({
dataTypes: {
'custom-type': {
set: function (newVal) {
return {
type: 'custom-type',
val: newVal
};
},
onChange: function (newVal, curVal, name) {
t.equal(newVal.value, 100, 'should get the default value as newVal');
t.equal(curVal, undefined, 'should get undefined as current value');
t.equal(name, 'strength', 'should get the attribute name');
t.pass('onChange was called');
}
}
},
props: {
strength: {
type: 'custom-type',
default: function () {
t.pass('default function should be called');
return {
value: 100
};
}
}
}
});

t.plan(6);
var p = new Person();
t.equal(p.strength.value, 100);
});

0 comments on commit beebc91

Please sign in to comment.