Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

custom datatype onChange function #202

Merged
merged 5 commits into from
Jan 25, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,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 @@ -217,6 +219,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 @@ -353,6 +356,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 @@ -577,9 +586,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 @@ -699,22 +712,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 @@ -1814,3 +1814,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);
});