ampersand-state
Advanced tools
Comparing version 4.8.2 to 4.9.0
@@ -23,2 +23,3 @@ 'use strict'; | ||
var changeRE = /^change:/; | ||
var noop = function () {}; | ||
@@ -120,4 +121,4 @@ function Base(attrs, options) { | ||
var extraProperties = this.extraProperties; | ||
var changing, changes, newType, newVal, def, cast, err, attr, | ||
attrs, dataType, silent, unset, currentVal, initial, hasChanged, isEqual; | ||
var wasChanging, changeEvents, newType, newVal, def, cast, err, attr, | ||
attrs, dataType, silent, unset, currentVal, initial, hasChanged, isEqual, onChange; | ||
@@ -142,8 +143,11 @@ // Handle both `"key", value` and `{key: value}` -style arguments. | ||
changes = []; | ||
changing = this._changing; | ||
// Initialize change tracking. | ||
wasChanging = this._changing; | ||
this._changing = true; | ||
changeEvents = []; | ||
// if not already changing, store previous | ||
if (!changing) { | ||
if (initial) { | ||
this._previousAttributes = {}; | ||
} else if (!wasChanging) { | ||
this._previousAttributes = this.attributes; | ||
@@ -154,3 +158,4 @@ this._changed = {}; | ||
// For each `set` attribute... | ||
for (attr in attrs) { | ||
for (var i = 0, keys = Object.keys(attrs), len = keys.length; i < len; i++) { | ||
attr = keys[i]; | ||
newVal = attrs[attr]; | ||
@@ -178,2 +183,3 @@ newType = typeof newVal; | ||
isEqual = this._getCompareForType(def.type); | ||
onChange = this._getOnChangeForType(def.type); | ||
dataType = this._dataTypes[def.type]; | ||
@@ -219,44 +225,54 @@ | ||
hasChanged = !isEqual(currentVal, newVal, attr); | ||
// We know this has 'changed' if it's the initial set, so skip a potentially expensive isEqual check. | ||
hasChanged = initial || !isEqual(currentVal, newVal, attr); | ||
// enforce `setOnce` for properties if set | ||
if (def.setOnce && currentVal !== undefined && hasChanged && !initial) { | ||
if (def.setOnce && currentVal !== undefined && hasChanged) { | ||
throw new TypeError('Property \'' + attr + '\' can only be set once.'); | ||
} | ||
// keep track of changed attributes | ||
// and push to changes array | ||
// set/unset attributes. | ||
// If this is not the initial set, keep track of changed attributes | ||
// and push to changeEvents array so we can fire events. | ||
if (hasChanged) { | ||
changes.push({prev: currentVal, val: newVal, key: attr}); | ||
self._changed[attr] = newVal; | ||
// This fires no matter what, even on initial set. | ||
onChange(newVal, currentVal, attr); | ||
// If this is a change (not an initial set), mark the change. | ||
// Note it's impossible to unset on the initial set (it will already be unset), | ||
// so we only include that logic here. | ||
if (!initial) { | ||
this._changed[attr] = newVal; | ||
this._previousAttributes[attr] = currentVal; | ||
if (unset) { | ||
// FIXME delete is very slow. Can we get away with setting to undefined? | ||
delete this._values[attr]; | ||
} | ||
if (!silent) { | ||
changeEvents.push({prev: currentVal, val: newVal, key: attr}); | ||
} | ||
} | ||
if (!unset) { | ||
this._values[attr] = newVal; | ||
} | ||
} else { | ||
delete self._changed[attr]; | ||
// Not changed | ||
// FIXME delete is very slow. Can we get away with setting to undefined? | ||
delete this._changed[attr]; | ||
} | ||
} | ||
// actually update our values | ||
changes.forEach(function (change) { | ||
self._previousAttributes[change.key] = change.prev; | ||
if (unset) { | ||
delete self._values[change.key]; | ||
} else { | ||
self._values[change.key] = change.val; | ||
} | ||
// Fire events. This array is not populated if we are told to be silent. | ||
if (changeEvents.length) this._pending = true; | ||
changeEvents.forEach(function (change) { | ||
self.trigger('change:' + change.key, self, change.val, options); | ||
}); | ||
if (!silent && changes.length) self._pending = true; | ||
if (!silent) { | ||
changes.forEach(function (change) { | ||
self.trigger('change:' + change.key, self, change.val, options); | ||
}); | ||
} | ||
// You might be wondering why there's a `while` loop here. Changes can | ||
// be recursively nested within `"change"` events. | ||
if (changing) return this; | ||
if (!silent) { | ||
while (this._pending) { | ||
this._pending = false; | ||
this.trigger('change', this, options); | ||
} | ||
if (wasChanging) return this; | ||
while (this._pending) { | ||
this._pending = false; | ||
this.trigger('change', this, options); | ||
} | ||
@@ -374,2 +390,8 @@ this._pending = false; | ||
_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, | ||
@@ -404,4 +426,4 @@ // returning `true` if all is well. Otherwise, fire an `"invalid"` event. | ||
var res = {}; | ||
var val, item, def; | ||
for (item in this._definition) { | ||
var val, def; | ||
for (var item in this._definition) { | ||
def = this._definition[item]; | ||
@@ -416,3 +438,3 @@ if ((options.session && def.session) || (options.props && !def.session)) { | ||
if (options.derived) { | ||
for (item in this._derived) res[item] = this[item]; | ||
for (var derivedItem in this._derived) res[derivedItem] = this[derivedItem]; | ||
} | ||
@@ -606,5 +628,11 @@ return res; | ||
} | ||
value = result(def, 'default'); | ||
this._values[name] = value; | ||
return value; | ||
var defaultValue = result(def, 'default'); | ||
this._values[name] = defaultValue; | ||
// If we've set a defaultValue, fire a change handler effectively marking | ||
// its change from undefined to the default value. | ||
if (typeof defaultValue !== 'undefined') { | ||
var onChange = this._getOnChangeForType(def.type); | ||
onChange(defaultValue, value, name); | ||
} | ||
return defaultValue; | ||
} | ||
@@ -729,18 +757,16 @@ }); | ||
}, | ||
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 (previousVal) { | ||
this.stopListening(previousVal); | ||
} | ||
if (newVal != null) { | ||
this.listenTo(newVal, 'all', this._getEventBubblingHandler(attributeName)); | ||
} | ||
if (newVal != null) { | ||
this.listenTo(newVal, 'all', this._getEventBubblingHandler(attributeName)); | ||
} | ||
return isSame; | ||
} | ||
@@ -747,0 +773,0 @@ } |
{ | ||
"name": "ampersand-state", | ||
"description": "An observable, extensible state object with derived watchable properties.", | ||
"version": "4.8.2", | ||
"version": "4.9.0", | ||
"author": "Henrik Joreteg <henrik@andyet.net>", | ||
@@ -42,12 +42,9 @@ "files": [ | ||
"ampersand-registry": "0.x.x", | ||
"browserify": "^11.0.1", | ||
"coveralls": "^2.11.4", | ||
"istanbul": "^0.4.0", | ||
"jshint": "^2.5.3", | ||
"phantomjs": "^1.9.7-15", | ||
"phantomjs": "^1.9.19", | ||
"precommit-hook": "^3.0.0", | ||
"run-browser": "^2.0.2", | ||
"tap-spec": "^4.0.2", | ||
"tape": "^4.0.3", | ||
"tape-run": "^1.1.0" | ||
"zuul": "^3.9.0" | ||
}, | ||
@@ -67,8 +64,9 @@ "homepage": "https://github.com/ampersandjs/ampersand-state", | ||
"scripts": { | ||
"test": "browserify test/index.js | tape-run | tap-spec", | ||
"start": "zuul --local -- test/index.js", | ||
"test": "zuul --phantom -- test/index.js", | ||
"test-ci": "zuul -- test/index.js", | ||
"coverage": "rm -rf coverage && istanbul cover -- tape test/index.js --verbose", | ||
"validate": "npm ls", | ||
"start": "run-browser test/index.js", | ||
"lint": "jshint ampersand-state.js ./test/*", | ||
"benchmark": "node --allow-natives-syntax benchmark/massCreate.js", | ||
"benchmark": "for f in benchmark/*.js; do node --allow-natives-syntax --trace-deopt $f; done", | ||
"preversion": "git checkout master && git pull && npm ls", | ||
@@ -80,18 +78,2 @@ "publish-patch": "npm run preversion && npm version patch && git push origin master --tags && npm publish", | ||
}, | ||
"testling": { | ||
"files": "test/*.js", | ||
"browsers": [ | ||
"ie/9..latest", | ||
"firefox/17..latest", | ||
"firefox/nightly", | ||
"chrome/22..latest", | ||
"chrome/canary", | ||
"opera/12..latest", | ||
"opera/next", | ||
"safari/5.1..latest", | ||
"ipad/6.0..latest", | ||
"iphone/6.0..latest", | ||
"android-browser/4.2..latest" | ||
] | ||
}, | ||
"pre-commit": [ | ||
@@ -98,0 +80,0 @@ "lint", |
@@ -226,2 +226,3 @@ # ampersand-state | ||
* `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. | ||
@@ -313,2 +314,3 @@ * `default : function(){} returns val;`: Returns the default value for this type. | ||
* If the property doesn't have a `default`, and you don't set the value initially, it can be set later, but only once. | ||
* If `test` function is passed, then a negative validation test will be executed every time this property is about to be set. If the validation passes, the function must return `false` to tell **State** to go ahead and set the value. Otherwise, it should return a `string` with the error message describing the validation failure. In this case **State** will throw a `TypeError` with `"Property '<property>' failed validation with error: <errorMessage>"`. | ||
@@ -328,3 +330,12 @@ Trying to set a property to an invalid type will throw an error. | ||
values: ['regular-hero', 'super-hero', 'mega-hero'] | ||
} | ||
}, | ||
numberOfChildren: { | ||
type: 'number', | ||
test: function(value){ | ||
if (value < 0) { | ||
return "Must be a positive number"; | ||
} | ||
return false; | ||
} | ||
}, | ||
} | ||
@@ -378,9 +389,19 @@ }); | ||
Derived properties (also known as computed properties) are properties of the state object that depend on other properties (from `props`, `session`, or even `derived`) to determine their value. Best demonstrated with an example: | ||
Derived properties (also known as computed properties) are properties of the state object that depend on other properties (from `props`, `session`, or even `derived` or the same from state props or children) to determine their value. Best demonstrated with an example: | ||
```javascript | ||
var Address = AmpersandState.extend({ | ||
props: { | ||
'street': 'string', | ||
'city': 'string', | ||
'region': 'string', | ||
'postcode': 'string' | ||
} | ||
}); | ||
var Person = AmpersandState.extend({ | ||
props: { | ||
firstName: 'string', | ||
lastName: 'string' | ||
lastName: 'string', | ||
address: 'state' | ||
}, | ||
@@ -393,2 +414,15 @@ derived: { | ||
} | ||
}, | ||
mailingAddress: { | ||
deps: ['address.street', 'address.city', 'address.region', 'address.postcode'], | ||
fn: function () { | ||
var self = this; | ||
return ['street','city','region','postcode'].map(function (prop) { | ||
var val = self.address[prop]; | ||
if (!val) return val; | ||
return (prop === 'street' || prop === 'city') ? val + ',' : val; | ||
}).filter(function (val) { | ||
return !!val; | ||
}).join(' '); | ||
} | ||
} | ||
@@ -398,9 +432,23 @@ } | ||
var person = new Person({ firstName: 'Phil', lastName: 'Roberts' }); | ||
var person = new Person({ | ||
firstName: 'Phil', | ||
lastName: 'Roberts', | ||
address: new Address({ | ||
street: '123 Main St', | ||
city: 'Anyplace', | ||
region: 'BC', | ||
postcode: 'V6A 2S5' | ||
}) | ||
}); | ||
console.log(person.fullName) //=> "Phil Roberts" | ||
console.log(person.mailingAddress) //=> "123 Main St, Anyplace, BC V6A 2S5" | ||
person.firstName = 'Bob'; | ||
person.address.street = '321 St. Charles Pl' | ||
console.log(person.fullName) //=> "Bob Roberts" | ||
console.log(person.mailingAddress) //=> "321 St. Charles Pl, Anyplace, BC V6A 2S5" | ||
``` | ||
See working example at [RequireBin](http://requirebin.com/?gist=c496f0d33f32527fe1ca) | ||
Each derived property is defined as an object with the following properties: | ||
@@ -407,0 +455,0 @@ |
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
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
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
66214
9
759
763
1