diff --git a/README.md b/README.md
index e3a2902..1b327ed 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,6 @@
NOTE: I no longer have time to maintain this module. Please see the forks list for actively maintained projects, such as https://github.com/kahwee/backbone-deep-model
-backbone-deep-model
-===================
+# backbone-deep-model
Improved support for models with nested attributes.
@@ -9,19 +8,17 @@ Allows you to get and set nested attributes with path syntax, e.g. `user.type`.
Triggers change events for changes on nested attributes.
-Dependencies
-============
+# Dependencies
Backbone >= 0.9.10
+Underscore >= 1.8.2
-Installation
-============
+# Installation
1. Include Backbone and it's dependencies in your page/app.
2. Include `distribution/deep-model.min.js`
-Usage
-=====
+# Usage
Then just have your models extend from Backbone.DeepModel instead of Backbone.Model.
@@ -42,7 +39,7 @@ Example code:
{ name: 'Cyrril' }
]
});
-
+
//You can bind to change events on nested attributes
model.bind('change:user.name.first', function(model, val) {
console.log(val);
@@ -50,73 +47,79 @@ Example code:
//Wildcards are supported
model.bind('change:user.*', function() {});
-
+
//Use set with a path name for nested attributes
//NOTE you must you quotation marks around the key name when using a path
model.set({
'user.name.first': 'Lana',
'user.name.last': 'Kang'
});
-
+
//Use get() with path names so you can create getters later
console.log(model.get('user.type')); // 'Spy'
//You can use index notation to fetch from arrays
console.log(model.get('otherSpies.0.name')) //'Lana'
-Author
-======
+# Author
Charles Davison - [powmedia](http://github.com/powmedia)
-
-Contributors
-============
+# Contributors
- [mattyway](https://github.com/mattyway)
- [AsaAyers](https://github.com/AsaAyers)
-
-Changelog
-=========
+# Changelog
master:
+
- Add supprt for arrays in nested attributes (sqren)
-0.11.0:
+ 0.11.0:
+
- Trigger change events only once (restorer)
-0.10.4:
+ 0.10.4:
+
- Fix #68 Model or collection in attributes are eliminated when defaults are used
-0.10.0:
+ 0.10.0:
+
- Support Backbone 0.9.10
-0.9.0:
+ 0.9.0:
+
- Support Backbone 0.9.9 (mattyway)
- Add support for deep model defaults (mattyway)
-0.8.0:
+ 0.8.0:
+
- Allow nested attribute as Model identifier (milosdakic)
- Add AMD support (drd0rk)
-- Added "change:*" event triggers for ancestors of nested attributes. (jessehouchins)
+- Added "change:\*" event triggers for ancestors of nested attributes. (jessehouchins)
- JSHint linting (tony)
- Fix: undefined in setNested resulting from a recent change in backbone master. (evadnoob)
-0.7.4:
+ 0.7.4:
+
- Fix: #22 model.hasChanged() is not reset after change event fires
- Fix: #18 Setting a deep property to the same value deletes the property (mikefrey)
- Allow setting nested attributes inside an attribute whose value is currently null (sqs)
-0.7.3:
+ 0.7.3:
+
- Add DeepModel.keyPathSeparator to allow using custom path separators
-0.7.2:
+ 0.7.2:
+
- Check if the child object exists, but isn't an object. #12 (tgriesser)
-0.7.1:
+ 0.7.1:
+
- Model.changedAttributes now works with nested attributes.
- Fix getting attribute that is an empty object
-0.7:
+ 0.7:
+
- Update for Backbone v0.9.2
diff --git a/distribution/deep-model.js b/distribution/deep-model.js
index 5307ffc..ebcdca2 100644
--- a/distribution/deep-model.js
+++ b/distribution/deep-model.js
@@ -1,7 +1,7 @@
/*jshint expr:true eqnull:true */
/**
*
- * Backbone.DeepModel v0.10.4
+ * Backbone.DeepModel v0.11.0
*
* Copyright (c) 2013 Charles Davison, Pow Media Ltd
*
@@ -14,7 +14,7 @@
*
* Based on https://gist.github.com/echong/3861963
*/
-(function() {
+(function(_, Backbone) {
var arrays, basicObjects, deepClone, deepExtend, deepExtendCouple, isBasicObject,
__slice = [].slice;
@@ -26,6 +26,9 @@
if (obj instanceof Backbone.Collection || obj instanceof Backbone.Model) {
return obj;
}
+ if (_.isElement(obj)) {
+ return obj;
+ }
if (_.isDate(obj)) {
return new Date(obj.getTime());
}
@@ -117,322 +120,340 @@
deepExtend: deepExtend
});
-}).call(this);
+}).call(this, _, Backbone);
/**
* Main source
*/
-;(function(factory) {
- if (typeof define === 'function' && define.amd) {
- // AMD
- define(['underscore', 'backbone'], factory);
- } else {
- // globals
- factory(_, Backbone);
- }
-}(function(_, Backbone) {
-
- /**
- * Takes a nested object and returns a shallow object keyed with the path names
- * e.g. { "level1.level2": "value" }
- *
- * @param {Object} Nested object e.g. { level1: { level2: 'value' } }
- * @return {Object} Shallow object with path names e.g. { 'level1.level2': 'value' }
- */
- function objToPaths(obj) {
- var ret = {},
- separator = DeepModel.keyPathSeparator;
-
- for (var key in obj) {
- var val = obj[key];
-
- if (val && val.constructor === Object && !_.isEmpty(val)) {
- //Recursion for embedded objects
- var obj2 = objToPaths(val);
-
- for (var key2 in obj2) {
- var val2 = obj2[key2];
-
- ret[key + separator + key2] = val2;
- }
- } else {
- ret[key] = val;
- }
+; (function (factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD
+ define(['underscore', 'backbone'], factory);
+ } else if (typeof exports === 'object') {
+ // CommonJS
+ module.exports = factory(require('underscore'), require('backbone'));
+ } else {
+ // globals
+ factory(_, Backbone);
+ }
+}(function (_, Backbone) {
+
+ /**
+ * Takes a nested object and returns a shallow object keyed with the path names
+ * e.g. { "level1.level2": "value" }
+ *
+ * @param {Object} Nested object e.g. { level1: { level2: 'value' } }
+ * @return {Object} Shallow object with path names e.g. { 'level1.level2': 'value' }
+ */
+ function objToPaths(obj) {
+ var ret = {},
+ separator = DeepModel.keyPathSeparator;
+
+ for (var key in obj) {
+ var val = obj[key];
+
+ if (val && (val.constructor === Object || val.constructor === Array) && !_.isEmpty(val)) {
+ //Recursion for embedded objects
+ var obj2 = objToPaths(val);
+
+ for (var key2 in obj2) {
+ var val2 = obj2[key2];
+
+ ret[key + separator + key2] = val2;
}
-
- return ret;
+ } else {
+ ret[key] = val;
+ }
}
- /**
- * @param {Object} Object to fetch attribute from
- * @param {String} Object path e.g. 'user.name'
- * @return {Mixed}
- */
- function getNested(obj, path, return_exists) {
- var separator = DeepModel.keyPathSeparator;
+ return ret;
+ }
+
+ /**
+ * @param {Object} Object to fetch attribute from
+ * @param {String} Object path e.g. 'user.name'
+ * @return {Mixed}
+ */
+ function getNested(obj, path, return_exists) {
+ var separator = DeepModel.keyPathSeparator;
+
+ var fields = path ? path.split(separator) : [];
+ var result = obj;
+ return_exists || (return_exists === false);
+ for (var i = 0, n = fields.length; i < n; i++) {
+ if (return_exists && !_.has(result, fields[i])) {
+ return false;
+ }
+ result = result[fields[i]];
- var fields = path.split(separator);
- var result = obj;
- return_exists || (return_exists === false);
- for (var i = 0, n = fields.length; i < n; i++) {
- if (return_exists && !_.has(result, fields[i])) {
- return false;
- }
- result = result[fields[i]];
+ if (result == null && i < n - 1) {
+ result = {};
+ }
- if (result == null && i < n - 1) {
- result = {};
- }
-
- if (typeof result === 'undefined') {
- if (return_exists)
- {
- return true;
- }
- return result;
- }
- }
- if (return_exists)
- {
- return true;
+ if (typeof result === 'undefined') {
+ if (return_exists) {
+ return true;
}
return result;
+ }
}
+ if (return_exists) {
+ return true;
+ }
+ return result;
+ }
+
+ /**
+ * @param {Object} obj Object to fetch attribute from
+ * @param {String} path Object path e.g. 'user.name'
+ * @param {Object} [options] Options
+ * @param {Boolean} [options.unset] Whether to delete the value
+ * @param {Mixed} Value to set
+ */
+ function setNested(obj, path, val, options) {
+ options = options || {};
+
+ var separator = DeepModel.keyPathSeparator;
+
+ var fields = path ? path.split(separator) : [];
+ var result = obj;
+ for (var i = 0, n = fields.length; i < n && result !== undefined; i++) {
+ var field = fields[i];
+
+ //If the last in the path, set the value
+ if (i === n - 1) {
+ options.unset ? delete result[field] : result[field] = val;
+ } else {
+ //Create the child object if it doesn't exist, or isn't an object
+ if (typeof result[field] === 'undefined' || !_.isObject(result[field])) {
+ var nextField = fields[i + 1];
- /**
- * @param {Object} obj Object to fetch attribute from
- * @param {String} path Object path e.g. 'user.name'
- * @param {Object} [options] Options
- * @param {Boolean} [options.unset] Whether to delete the value
- * @param {Mixed} Value to set
- */
- function setNested(obj, path, val, options) {
- options = options || {};
-
- var separator = DeepModel.keyPathSeparator;
-
- var fields = path.split(separator);
- var result = obj;
- for (var i = 0, n = fields.length; i < n && result !== undefined ; i++) {
- var field = fields[i];
-
- //If the last in the path, set the value
- if (i === n - 1) {
- options.unset ? delete result[field] : result[field] = val;
- } else {
- //Create the child object if it doesn't exist, or isn't an object
- if (typeof result[field] === 'undefined' || ! _.isObject(result[field])) {
- result[field] = {};
- }
-
- //Move onto the next part of the path
- result = result[field];
- }
+ // create array if next field is integer, else create object
+ result[field] = /^\d+$/.test(nextField) ? [] : {};
}
- }
- function deleteNested(obj, path) {
- setNested(obj, path, null, { unset: true });
+ //Move onto the next part of the path
+ result = result[field];
+ }
}
+ }
+
+ function deleteNested(obj, path) {
+ setNested(obj, path, null, { unset: true });
+ }
+
+ var DeepModel = Backbone.Model.extend({
+
+ // Override constructor
+ // Support having nested defaults by using _.deepExtend instead of _.extend
+ constructor: function (attributes, options) {
+ var defaults;
+ var attrs = attributes || {};
+ this.cid = _.uniqueId('c');
+ this.attributes = {};
+ if (options && options.collection) this.collection = options.collection;
+ if (options && options.parse) attrs = this.parse(attrs, options) || {};
+ if (defaults = _.result(this, 'defaults')) {
+ //
+ // Replaced the call to _.defaults with _.deepExtend.
+ attrs = _.deepExtend({}, defaults, attrs);
+ //
+ }
+ this.set(attrs, options);
+ this.changed = {};
+ this.initialize.apply(this, arguments);
+ },
+
+ // Return a copy of the model's `attributes` object.
+ toJSON: function (options) {
+ return _.deepClone(this.attributes);
+ },
+
+ // Override get
+ // Supports nested attributes via the syntax 'obj.attr' e.g. 'author.user.name'
+ get: function (attr) {
+ return getNested(this.attributes, attr);
+ },
+
+ // Override set
+ // Supports nested attributes via the syntax 'obj.attr' e.g. 'author.user.name'
+ set: function (key, val, options) {
+ var attr, attrs, unset, changes, silent, changing, prev, current;
+ if (key == null) return this;
+
+ // Handle both `"key", value` and `{key: value}` -style arguments.
+ if (typeof key === 'object') {
+ attrs = key;
+ options = val || {};
+ } else {
+ (attrs = {})[key] = val;
+ }
- var DeepModel = Backbone.Model.extend({
-
- // Override constructor
- // Support having nested defaults by using _.deepExtend instead of _.extend
- constructor: function(attributes, options) {
- var defaults;
- var attrs = attributes || {};
- this.cid = _.uniqueId('c');
- this.attributes = {};
- if (options && options.collection) this.collection = options.collection;
- if (options && options.parse) attrs = this.parse(attrs, options) || {};
- if (defaults = _.result(this, 'defaults')) {
- //
- // Replaced the call to _.defaults with _.deepExtend.
- attrs = _.deepExtend({}, defaults, attrs);
- //
- }
- this.set(attrs, options);
- this.changed = {};
- this.initialize.apply(this, arguments);
- },
-
- // Return a copy of the model's `attributes` object.
- toJSON: function(options) {
- return _.deepClone(this.attributes);
- },
-
- // Override get
- // Supports nested attributes via the syntax 'obj.attr' e.g. 'author.user.name'
- get: function(attr) {
- return getNested(this.attributes, attr);
- },
-
- // Override set
- // Supports nested attributes via the syntax 'obj.attr' e.g. 'author.user.name'
- set: function(key, val, options) {
- var attr, attrs, unset, changes, silent, changing, prev, current;
- if (key == null) return this;
-
- // Handle both `"key", value` and `{key: value}` -style arguments.
- if (typeof key === 'object') {
- attrs = key;
- options = val || {};
- } else {
- (attrs = {})[key] = val;
- }
+ options || (options = {});
- options || (options = {});
-
- // Run validation.
- if (!this._validate(attrs, options)) return false;
-
- // Extract attributes and options.
- unset = options.unset;
- silent = options.silent;
- changes = [];
- changing = this._changing;
- this._changing = true;
-
- if (!changing) {
- this._previousAttributes = _.deepClone(this.attributes); //: Replaced _.clone with _.deepClone
- this.changed = {};
- }
- current = this.attributes, prev = this._previousAttributes;
-
- // Check for changes of `id`.
- if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
-
- //
- attrs = objToPaths(attrs);
- //
-
- // For each `set` attribute, update or delete the current value.
- for (attr in attrs) {
- val = attrs[attr];
-
- //: Using getNested, setNested and deleteNested
- if (!_.isEqual(getNested(current, attr), val)) changes.push(attr);
- if (!_.isEqual(getNested(prev, attr), val)) {
- setNested(this.changed, attr, val);
- } else {
- deleteNested(this.changed, attr);
- }
- unset ? deleteNested(current, attr) : setNested(current, attr, val);
- //
- }
+ // Run validation.
+ if (!this._validate(attrs, options)) return false;
- // Trigger all relevant attribute changes.
- if (!silent) {
- if (changes.length) this._pending = true;
+ // Extract attributes and options.
+ unset = options.unset;
+ silent = options.silent;
+ changes = [];
+ changing = this._changing;
+ this._changing = true;
- //
- var separator = DeepModel.keyPathSeparator;
+ if (!changing) {
+ this._previousAttributes = _.deepClone(this.attributes); //: Replaced _.clone with _.deepClone
+ this.changed = {};
+ }
+ current = this.attributes, prev = this._previousAttributes;
- for (var i = 0, l = changes.length; i < l; i++) {
- var key = changes[i];
+ // Check for changes of `id`.
+ if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
- this.trigger('change:' + key, this, getNested(current, key), options);
+ //
+ attrs = objToPaths(attrs);
+ //
- var fields = key.split(separator);
+ // For each `set` attribute, update or delete the current value.
+ for (attr in attrs) {
+ val = attrs[attr];
- //Trigger change events for parent keys with wildcard (*) notation
- for(var n = fields.length - 1; n > 0; n--) {
- var parentKey = _.first(fields, n).join(separator),
- wildcardKey = parentKey + separator + '*';
+ //: Using getNested, setNested and deleteNested
+ if (!_.isEqual(getNested(current, attr), val)) changes.push(attr);
+ if (!_.isEqual(getNested(prev, attr), val)) {
+ setNested(this.changed, attr, val);
+ } else {
+ deleteNested(this.changed, attr);
+ }
+ unset ? deleteNested(current, attr) : setNested(current, attr, val);
+ //
+ }
- this.trigger('change:' + wildcardKey, this, getNested(current, parentKey), options);
- }
- //
- }
- }
+ // Trigger all relevant attribute changes.
+ if (!silent) {
+ if (changes.length) this._pending = true;
- if (changing) return this;
- if (!silent) {
- while (this._pending) {
- this._pending = false;
- this.trigger('change', this, options);
- }
- }
- this._pending = false;
- this._changing = false;
- return this;
- },
-
- // Clear all attributes on the model, firing `"change"` unless you choose
- // to silence it.
- clear: function(options) {
- var attrs = {};
- var shallowAttributes = objToPaths(this.attributes);
- for (var key in shallowAttributes) attrs[key] = void 0;
- return this.set(attrs, _.extend({}, options, {unset: true}));
- },
-
- // Determine if the model has changed since the last `"change"` event.
- // If you specify an attribute name, determine if that attribute has changed.
- hasChanged: function(attr) {
- if (attr == null) return !_.isEmpty(this.changed);
- return getNested(this.changed, attr) !== undefined;
- },
-
- // Return an object containing all the attributes that have changed, or
- // false if there are no changed attributes. Useful for determining what
- // parts of a view need to be updated and/or what attributes need to be
- // persisted to the server. Unset attributes will be set to undefined.
- // You can also pass an attributes object to diff against the model,
- // determining if there *would be* a change.
- changedAttributes: function(diff) {
- //: objToPaths
- if (!diff) return this.hasChanged() ? objToPaths(this.changed) : false;
- //
+ //
+ var separator = DeepModel.keyPathSeparator;
+ var alreadyTriggered = {}; // * @restorer
- var old = this._changing ? this._previousAttributes : this.attributes;
-
- //
- diff = objToPaths(diff);
- old = objToPaths(old);
- //
+ for (var i = 0, l = changes.length; i < l; i++) {
+ var key = changes[i];
- var val, changed = false;
- for (var attr in diff) {
- if (_.isEqual(old[attr], (val = diff[attr]))) continue;
- (changed || (changed = {}))[attr] = val;
- }
- return changed;
- },
+ if (!alreadyTriggered.hasOwnProperty(key) || !alreadyTriggered[key]) { // * @restorer
+ alreadyTriggered[key] = true; // * @restorer
+ this.trigger('change:' + key, this, getNested(current, key), options);
+ } // * @restorer
- // Get the previous value of an attribute, recorded at the time the last
- // `"change"` event was fired.
- previous: function(attr) {
- if (attr == null || !this._previousAttributes) return null;
+ var fields = key.split(separator);
- //
- return getNested(this._previousAttributes, attr);
- //
- },
+ //Trigger change events for parent keys with wildcard (*) notation
+ for (var n = fields.length - 1; n > 0; n--) {
+ var parentKey = _.first(fields, n).join(separator),
+ wildcardKey = parentKey + separator + '*';
+
+ if (!alreadyTriggered.hasOwnProperty(wildcardKey) || !alreadyTriggered[wildcardKey]) { // * @restorer
+ alreadyTriggered[wildcardKey] = true; // * @restorer
+ this.trigger('change:' + wildcardKey, this, getNested(current, parentKey), options);
+ } // * @restorer
- // Get all of the attributes of the model at the time of the previous
- // `"change"` event.
- previousAttributes: function() {
- //
- return _.deepClone(this._previousAttributes);
+ // + @restorer
+ if (!alreadyTriggered.hasOwnProperty(parentKey) || !alreadyTriggered[parentKey]) {
+ alreadyTriggered[parentKey] = true;
+ this.trigger('change:' + parentKey, this, getNested(current, parentKey), options);
+ }
+ // - @restorer
+ }
//
}
- });
+ }
+
+ if (changing) return this;
+ if (!silent) {
+ while (this._pending) {
+ this._pending = false;
+ this.trigger('change', this, options);
+ }
+ }
+ this._pending = false;
+ this._changing = false;
+ return this;
+ },
+
+ // Clear all attributes on the model, firing `"change"` unless you choose
+ // to silence it.
+ clear: function (options) {
+ var attrs = {};
+ var shallowAttributes = objToPaths(this.attributes);
+ for (var key in shallowAttributes) attrs[key] = void 0;
+ return this.set(attrs, _.extend({}, options, { unset: true }));
+ },
+
+ // Determine if the model has changed since the last `"change"` event.
+ // If you specify an attribute name, determine if that attribute has changed.
+ hasChanged: function (attr) {
+ if (attr == null) return !_.isEmpty(this.changed);
+ return getNested(this.changed, attr) !== undefined;
+ },
+
+ // Return an object containing all the attributes that have changed, or
+ // false if there are no changed attributes. Useful for determining what
+ // parts of a view need to be updated and/or what attributes need to be
+ // persisted to the server. Unset attributes will be set to undefined.
+ // You can also pass an attributes object to diff against the model,
+ // determining if there *would be* a change.
+ changedAttributes: function (diff) {
+ //: objToPaths
+ if (!diff) return this.hasChanged() ? objToPaths(this.changed) : false;
+ //
+
+ var old = this._changing ? this._previousAttributes : this.attributes;
+
+ //
+ diff = objToPaths(diff);
+ old = objToPaths(old);
+ //
+
+ var val, changed = false;
+ for (var attr in diff) {
+ if (_.isEqual(old[attr], (val = diff[attr]))) continue;
+ (changed || (changed = {}))[attr] = val;
+ }
+ return changed;
+ },
+
+ // Get the previous value of an attribute, recorded at the time the last
+ // `"change"` event was fired.
+ previous: function (attr) {
+ if (attr == null || !this._previousAttributes) return null;
+
+ //
+ return getNested(this._previousAttributes, attr);
+ //
+ },
+
+ // Get all of the attributes of the model at the time of the previous
+ // `"change"` event.
+ previousAttributes: function () {
+ //
+ return _.deepClone(this._previousAttributes);
+ //
+ }
+ });
+
+ //Config; override in your app to customise
+ DeepModel.keyPathSeparator = '.';
- //Config; override in your app to customise
- DeepModel.keyPathSeparator = '.';
+ //Exports
+ Backbone.DeepModel = DeepModel;
- //Exports
- Backbone.DeepModel = DeepModel;
+ //For use in NodeJS
+ if (typeof module != 'undefined') module.exports = DeepModel;
- //For use in NodeJS
- if (typeof module != 'undefined') module.exports = DeepModel;
-
- return Backbone;
+ return Backbone;
}));
diff --git a/distribution/deep-model.min.js b/distribution/deep-model.min.js
index e488ad5..1055cfd 100644
--- a/distribution/deep-model.min.js
+++ b/distribution/deep-model.min.js
@@ -1,7 +1,7 @@
/*jshint expr:true eqnull:true */
/**
*
- * Backbone.DeepModel v0.10.4
+ * Backbone.DeepModel v0.11.0
*
* Copyright (c) 2013 Charles Davison, Pow Media Ltd
*
@@ -9,4 +9,4 @@
* Licensed under the MIT License
*/
-(function(){var e,t,n,r,i,s,o=[].slice;n=function(e){var t,r;return!_.isObject(e)||_.isFunction(e)?e:e instanceof Backbone.Collection||e instanceof Backbone.Model?e:_.isDate(e)?new Date(e.getTime()):_.isRegExp(e)?new RegExp(e.source,e.toString().replace(/.*\//,"")):(r=_.isArray(e||_.isArguments(e)),t=function(e,t,i){return r?e.push(n(t)):e[i]=n(t),e},_.reduce(e,t,r?[]:{}))},s=function(e){return e==null?!1:(e.prototype==={}.prototype||e.prototype===Object.prototype)&&_.isObject(e)&&!_.isArray(e)&&!_.isFunction(e)&&!_.isDate(e)&&!_.isRegExp(e)&&!_.isArguments(e)},t=function(e){return _.filter(_.keys(e),function(t){return s(e[t])})},e=function(e){return _.filter(_.keys(e),function(t){return _.isArray(e[t])})},i=function(n,r,s){var o,u,a,f,l,c,h,p,d,v;s==null&&(s=20);if(s<=0)return console.warn("_.deepExtend(): Maximum depth of recursion hit."),_.extend(n,r);c=_.intersection(t(n),t(r)),u=function(e){return r[e]=i(n[e],r[e],s-1)};for(h=0,d=c.length;h0)e=i(e,n(r.shift()),t);return e},_.mixin({deepClone:n,isBasicObject:s,basicObjects:t,arrays:e,deepExtend:r})}).call(this),function(e){typeof define=="function"&&define.amd?define(["underscore","backbone"],e):e(_,Backbone)}(function(e,t){function n(t){var r={},i=o.keyPathSeparator;for(var s in t){var u=t[s];if(u&&u.constructor===Object&&!e.isEmpty(u)){var a=n(u);for(var f in a){var l=a[f];r[s+i+f]=l}}else r[s]=u}return r}function r(t,n,r){var i=o.keyPathSeparator,s=n.split(i),u=t;r||r===!1;for(var a=0,f=s.length;a0;E--){var S=e.first(w,E).join(g),x=S+g+"*";this.trigger("change:"+x,this,r(m,S),a)}}}if(d)return this;if(!p)while(this._pending)this._pending=!1,this.trigger("change",this,a);return this._pending=!1,this._changing=!1,this},clear:function(t){var r={},i=n(this.attributes);for(var s in i)r[s]=void 0;return this.set(r,e.extend({},t,{unset:!0}))},hasChanged:function(t){return t==null?!e.isEmpty(this.changed):r(this.changed,t)!==undefined},changedAttributes:function(t){if(!t)return this.hasChanged()?n(this.changed):!1;var r=this._changing?this._previousAttributes:this.attributes;t=n(t),r=n(r);var i,s=!1;for(var o in t){if(e.isEqual(r[o],i=t[o]))continue;(s||(s={}))[o]=i}return s},previous:function(e){return e==null||!this._previousAttributes?null:r(this._previousAttributes,e)},previousAttributes:function(){return e.deepClone(this._previousAttributes)}});return o.keyPathSeparator=".",t.DeepModel=o,typeof module!="undefined"&&(module.exports=o),t})
+(function(e,t){var n,r,i,s,o,u,a=[].slice;i=function(n){var r,s;return!e.isObject(n)||e.isFunction(n)?n:n instanceof t.Collection||n instanceof t.Model?n:e.isElement(n)?n:e.isDate(n)?new Date(n.getTime()):e.isRegExp(n)?new RegExp(n.source,n.toString().replace(/.*\//,"")):(s=e.isArray(n||e.isArguments(n)),r=function(e,t,n){return s?e.push(i(t)):e[n]=i(t),e},e.reduce(n,r,s?[]:{}))},u=function(t){return t==null?!1:(t.prototype==={}.prototype||t.prototype===Object.prototype)&&e.isObject(t)&&!e.isArray(t)&&!e.isFunction(t)&&!e.isDate(t)&&!e.isRegExp(t)&&!e.isArguments(t)},r=function(t){return e.filter(e.keys(t),function(e){return u(t[e])})},n=function(t){return e.filter(e.keys(t),function(n){return e.isArray(t[n])})},o=function(t,i,s){var u,a,f,l,c,h,p,d,v,m;s==null&&(s=20);if(s<=0)return console.warn("_.deepExtend(): Maximum depth of recursion hit."),e.extend(t,i);h=e.intersection(r(t),r(i)),a=function(e){return i[e]=o(t[e],i[e],s-1)};for(p=0,v=h.length;p0)t=o(t,i(r.shift()),n);return t},e.mixin({deepClone:i,isBasicObject:u,basicObjects:r,arrays:n,deepExtend:s})}).call(this,_,Backbone),function(e){typeof define=="function"&&define.amd?define(["underscore","backbone"],e):typeof exports=="object"?module.exports=e(require("underscore"),require("backbone")):e(_,Backbone)}(function(e,t){function n(t){var r={},i=o.keyPathSeparator;for(var s in t){var u=t[s];if(u&&(u.constructor===Object||u.constructor===Array)&&!e.isEmpty(u)){var a=n(u);for(var f in a){var l=a[f];r[s+i+f]=l}}else r[s]=u}return r}function r(t,n,r){var i=o.keyPathSeparator,s=n?n.split(i):[],u=t;r||r===!1;for(var a=0,f=s.length;a0;S--){var x=e.first(E,S).join(g),T=x+g+"*";if(!y.hasOwnProperty(T)||!y[T])y[T]=!0,this.trigger("change:"+T,this,r(m,x),a);if(!y.hasOwnProperty(x)||!y[x])y[x]=!0,this.trigger("change:"+x,this,r(m,x),a)}}}if(d)return this;if(!p)while(this._pending)this._pending=!1,this.trigger("change",this,a);return this._pending=!1,this._changing=!1,this},clear:function(t){var r={},i=n(this.attributes);for(var s in i)r[s]=void 0;return this.set(r,e.extend({},t,{unset:!0}))},hasChanged:function(t){return t==null?!e.isEmpty(this.changed):r(this.changed,t)!==undefined},changedAttributes:function(t){if(!t)return this.hasChanged()?n(this.changed):!1;var r=this._changing?this._previousAttributes:this.attributes;t=n(t),r=n(r);var i,s=!1;for(var o in t){if(e.isEqual(r[o],i=t[o]))continue;(s||(s={}))[o]=i}return s},previous:function(e){return e==null||!this._previousAttributes?null:r(this._previousAttributes,e)},previousAttributes:function(){return e.deepClone(this._previousAttributes)}});return o.keyPathSeparator=".",t.DeepModel=o,typeof module!="undefined"&&(module.exports=o),t})
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..98fcbce
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,110 @@
+{
+ "name": "backbone-deep-model",
+ "version": "0.11.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "backbone-deep-model",
+ "version": "0.11.0",
+ "dependencies": {
+ "backbone": ">=0.9.10"
+ },
+ "devDependencies": {
+ "buildify": "latest"
+ }
+ },
+ "node_modules/backbone": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.6.0.tgz",
+ "integrity": "sha512-13PUjmsgw/49EowNcQvfG4gmczz1ximTMhUktj0Jfrjth0MVaTxehpU+qYYX4MxnuIuhmvBLC6/ayxuAGnOhbA==",
+ "dependencies": {
+ "underscore": ">=1.8.3"
+ }
+ },
+ "node_modules/buildify": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/buildify/-/buildify-0.4.0.tgz",
+ "integrity": "sha512-1te0l31Q4wBTLcw33n3uPJcBkm8Qagqc1J1HFhtz+0ZskCn4aZRNn0cHQgBPg9Y0Hvmz8IA+PH0vMNZyzNzV0w==",
+ "dev": true,
+ "dependencies": {
+ "clean-css": "0.6.0",
+ "mkdirp": "0.3.2",
+ "uglify-js": "1.3.4",
+ "underscore": "1.3.3"
+ },
+ "bin": {
+ "buildify": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/buildify/node_modules/underscore": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.3.3.tgz",
+ "integrity": "sha512-ddgUaY7xyrznJ0tbSUZgvNdv5qbiF6XcUBTrHgdCOVUrxJYWozD5KyiRjtIwds1reZ7O1iPLv5rIyqnVAcS6gg==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/clean-css": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-0.6.0.tgz",
+ "integrity": "sha512-qlaWMN7cgwixO2qfGjgB2HaQ16OD/HjIq+P5XVMFYLBDWdoQMT8YQpyLrJwv6+m49heKA/syHVdOCobFfsN1bQ==",
+ "dev": true,
+ "dependencies": {
+ "optimist": "0.3.x"
+ },
+ "bin": {
+ "cleancss": "bin/cleancss"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.2.tgz",
+ "integrity": "sha512-pc+27TvK2K/zCoLgoqHbCDndezId7Gbb00HjFACfXlFqOmbi+JyghsLmjiAGfDYrbGxQIlAWPM2UId3nx6JHOQ==",
+ "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/optimist": {
+ "version": "0.3.7",
+ "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz",
+ "integrity": "sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==",
+ "dev": true,
+ "dependencies": {
+ "wordwrap": "~0.0.2"
+ }
+ },
+ "node_modules/uglify-js": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-1.3.4.tgz",
+ "integrity": "sha512-96JciwD63/XR0T3oOe9Pob8ne9dfVjYO66yvZdXKD4mdKWwipVcn7pFBy9RDIJ/L4/3oOXox/F4VHbK3anhOpQ==",
+ "dev": true,
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ }
+ },
+ "node_modules/underscore": {
+ "version": "1.13.7",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
+ "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g=="
+ },
+ "node_modules/wordwrap": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
+ "integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
index 5d10288..9870e1a 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,9 @@
"author": {
"name": "Charles Davison"
},
+ "scripts": {
+ "build": "./scripts/build"
+ },
"version": "0.11.0",
"dependencies": {
"backbone": ">=0.9.10"
diff --git a/src/deep-model.js b/src/deep-model.js
index c7d24e1..7d90f15 100644
--- a/src/deep-model.js
+++ b/src/deep-model.js
@@ -2,335 +2,333 @@
* Main source
*/
-;(function(factory) {
- if (typeof define === 'function' && define.amd) {
- // AMD
- define(['underscore', 'backbone'], factory);
- } else if (typeof exports === 'object') {
- // CommonJS
- module.exports = factory(require('underscore'), require('backbone'));
- } else {
- // globals
- factory(_, Backbone);
- }
-}(function(_, Backbone) {
-
- /**
- * Takes a nested object and returns a shallow object keyed with the path names
- * e.g. { "level1.level2": "value" }
- *
- * @param {Object} Nested object e.g. { level1: { level2: 'value' } }
- * @return {Object} Shallow object with path names e.g. { 'level1.level2': 'value' }
- */
- function objToPaths(obj) {
- var ret = {},
- separator = DeepModel.keyPathSeparator;
-
- for (var key in obj) {
- var val = obj[key];
-
- if (val && (val.constructor === Object || val.constructor === Array) && !_.isEmpty(val)) {
- //Recursion for embedded objects
- var obj2 = objToPaths(val);
-
- for (var key2 in obj2) {
- var val2 = obj2[key2];
-
- ret[key + separator + key2] = val2;
- }
- } else {
- ret[key] = val;
- }
+; (function (factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD
+ define(['underscore', 'backbone'], factory);
+ } else if (typeof exports === 'object') {
+ // CommonJS
+ module.exports = factory(require('underscore'), require('backbone'));
+ } else {
+ // globals
+ factory(_, Backbone);
+ }
+}(function (_, Backbone) {
+
+ /**
+ * Takes a nested object and returns a shallow object keyed with the path names
+ * e.g. { "level1.level2": "value" }
+ *
+ * @param {Object} Nested object e.g. { level1: { level2: 'value' } }
+ * @return {Object} Shallow object with path names e.g. { 'level1.level2': 'value' }
+ */
+ function objToPaths(obj) {
+ var ret = {},
+ separator = DeepModel.keyPathSeparator;
+
+ for (var key in obj) {
+ var val = obj[key];
+
+ if (val && (val.constructor === Object || val.constructor === Array) && !_.isEmpty(val)) {
+ //Recursion for embedded objects
+ var obj2 = objToPaths(val);
+
+ for (var key2 in obj2) {
+ var val2 = obj2[key2];
+
+ ret[key + separator + key2] = val2;
}
-
- return ret;
+ } else {
+ ret[key] = val;
+ }
}
- /**
- * @param {Object} Object to fetch attribute from
- * @param {String} Object path e.g. 'user.name'
- * @return {Mixed}
- */
- function getNested(obj, path, return_exists) {
- var separator = DeepModel.keyPathSeparator;
-
- var fields = path ? path.split(separator) : [];
- var result = obj;
- return_exists || (return_exists === false);
- for (var i = 0, n = fields.length; i < n; i++) {
- if (return_exists && !_.has(result, fields[i])) {
- return false;
- }
- result = result[fields[i]];
-
- if (result == null && i < n - 1) {
- result = {};
- }
-
- if (typeof result === 'undefined') {
- if (return_exists)
- {
- return true;
- }
- return result;
- }
- }
- if (return_exists)
- {
- return true;
+ return ret;
+ }
+
+ /**
+ * @param {Object} Object to fetch attribute from
+ * @param {String} Object path e.g. 'user.name'
+ * @return {Mixed}
+ */
+ function getNested(obj, path, return_exists) {
+ var separator = DeepModel.keyPathSeparator;
+
+ var fields = path ? path.split(separator) : [];
+ var result = obj;
+ return_exists || (return_exists === false);
+ for (var i = 0, n = fields.length; i < n; i++) {
+ if (return_exists && !_.has(result, fields[i])) {
+ return false;
+ }
+ result = result[fields[i]];
+
+ if (result == null && i < n - 1) {
+ result = {};
+ }
+
+ if (typeof result === 'undefined') {
+ if (return_exists) {
+ return true;
}
return result;
+ }
}
-
- /**
- * @param {Object} obj Object to fetch attribute from
- * @param {String} path Object path e.g. 'user.name'
- * @param {Object} [options] Options
- * @param {Boolean} [options.unset] Whether to delete the value
- * @param {Mixed} Value to set
- */
- function setNested(obj, path, val, options) {
- options = options || {};
-
- var separator = DeepModel.keyPathSeparator;
-
- var fields = path ? path.split(separator) : [];
- var result = obj;
- for (var i = 0, n = fields.length; i < n && result !== undefined ; i++) {
- var field = fields[i];
-
- //If the last in the path, set the value
- if (i === n - 1) {
- options.unset ? delete result[field] : result[field] = val;
- } else {
- //Create the child object if it doesn't exist, or isn't an object
- if (typeof result[field] === 'undefined' || ! _.isObject(result[field])) {
- var nextField = fields[i+1];
-
- // create array if next field is integer, else create object
- result[field] = /^\d+$/.test(nextField) ? [] : {};
- }
-
- //Move onto the next part of the path
- result = result[field];
- }
- }
+ if (return_exists) {
+ return true;
}
+ return result;
+ }
+
+ /**
+ * @param {Object} obj Object to fetch attribute from
+ * @param {String} path Object path e.g. 'user.name'
+ * @param {Object} [options] Options
+ * @param {Boolean} [options.unset] Whether to delete the value
+ * @param {Mixed} Value to set
+ */
+ function setNested(obj, path, val, options) {
+ options = options || {};
+
+ var separator = DeepModel.keyPathSeparator;
+
+ var fields = path ? path.split(separator) : [];
+ var result = obj;
+ for (var i = 0, n = fields.length; i < n && result !== undefined; i++) {
+ var field = fields[i];
+
+ //If the last in the path, set the value
+ if (i === n - 1) {
+ options.unset ? delete result[field] : result[field] = val;
+ } else {
+ //Create the child object if it doesn't exist, or isn't an object
+ if (typeof result[field] === 'undefined' || !_.isObject(result[field])) {
+ var nextField = fields[i + 1];
+
+ // create array if next field is integer, else create object
+ result[field] = /^\d+$/.test(nextField) ? [] : {};
+ }
- function deleteNested(obj, path) {
- setNested(obj, path, null, { unset: true });
+ //Move onto the next part of the path
+ result = result[field];
+ }
}
+ }
+
+ function deleteNested(obj, path) {
+ setNested(obj, path, null, { unset: true });
+ }
+
+ var DeepModel = Backbone.Model.extend({
+
+ // Override constructor
+ // Support having nested defaults by using _.deepExtend instead of _.extend
+ constructor: function (attributes, options) {
+ var defaults;
+ var attrs = attributes || {};
+ this.cid = _.uniqueId('c');
+ this.attributes = {};
+ if (options && options.collection) this.collection = options.collection;
+ if (options && options.parse) attrs = this.parse(attrs, options) || {};
+ if (defaults = _.result(this, 'defaults')) {
+ //
+ // Replaced the call to _.defaults with _.deepExtend.
+ attrs = _.deepExtend({}, defaults, attrs);
+ //
+ }
+ this.set(attrs, options);
+ this.changed = {};
+ this.initialize.apply(this, arguments);
+ },
+
+ // Return a copy of the model's `attributes` object.
+ toJSON: function (options) {
+ return _.deepClone(this.attributes);
+ },
+
+ // Override get
+ // Supports nested attributes via the syntax 'obj.attr' e.g. 'author.user.name'
+ get: function (attr) {
+ return getNested(this.attributes, attr);
+ },
+
+ // Override set
+ // Supports nested attributes via the syntax 'obj.attr' e.g. 'author.user.name'
+ set: function (key, val, options) {
+ var attr, attrs, unset, changes, silent, changing, prev, current;
+ if (key == null) return this;
+
+ // Handle both `"key", value` and `{key: value}` -style arguments.
+ if (typeof key === 'object') {
+ attrs = key;
+ options = val || {};
+ } else {
+ (attrs = {})[key] = val;
+ }
+
+ options || (options = {});
+
+ // Run validation.
+ if (!this._validate(attrs, options)) return false;
+
+ // Extract attributes and options.
+ unset = options.unset;
+ silent = options.silent;
+ changes = [];
+ changing = this._changing;
+ this._changing = true;
+
+ if (!changing) {
+ this._previousAttributes = _.deepClone(this.attributes); //: Replaced _.clone with _.deepClone
+ this.changed = {};
+ }
+ current = this.attributes, prev = this._previousAttributes;
+
+ // Check for changes of `id`.
+ if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
+
+ //
+ attrs = objToPaths(attrs);
+ //
+
+ // For each `set` attribute, update or delete the current value.
+ for (attr in attrs) {
+ val = attrs[attr];
+
+ //: Using getNested, setNested and deleteNested
+ if (!_.isEqual(getNested(current, attr), val)) changes.push(attr);
+ if (!_.isEqual(getNested(prev, attr), val)) {
+ setNested(this.changed, attr, val);
+ } else {
+ deleteNested(this.changed, attr);
+ }
+ unset ? deleteNested(current, attr) : setNested(current, attr, val);
+ //
+ }
- var DeepModel = Backbone.Model.extend({
-
- // Override constructor
- // Support having nested defaults by using _.deepExtend instead of _.extend
- constructor: function(attributes, options) {
- var defaults;
- var attrs = attributes || {};
- this.cid = _.uniqueId('c');
- this.attributes = {};
- if (options && options.collection) this.collection = options.collection;
- if (options && options.parse) attrs = this.parse(attrs, options) || {};
- if (defaults = _.result(this, 'defaults')) {
- //
- // Replaced the call to _.defaults with _.deepExtend.
- attrs = _.deepExtend({}, defaults, attrs);
- //
- }
- this.set(attrs, options);
- this.changed = {};
- this.initialize.apply(this, arguments);
- },
-
- // Return a copy of the model's `attributes` object.
- toJSON: function(options) {
- return _.deepClone(this.attributes);
- },
-
- // Override get
- // Supports nested attributes via the syntax 'obj.attr' e.g. 'author.user.name'
- get: function(attr) {
- return getNested(this.attributes, attr);
- },
-
- // Override set
- // Supports nested attributes via the syntax 'obj.attr' e.g. 'author.user.name'
- set: function(key, val, options) {
- var attr, attrs, unset, changes, silent, changing, prev, current;
- if (key == null) return this;
-
- // Handle both `"key", value` and `{key: value}` -style arguments.
- if (typeof key === 'object') {
- attrs = key;
- options = val || {};
- } else {
- (attrs = {})[key] = val;
- }
-
- options || (options = {});
-
- // Run validation.
- if (!this._validate(attrs, options)) return false;
+ // Trigger all relevant attribute changes.
+ if (!silent) {
+ if (changes.length) this._pending = true;
- // Extract attributes and options.
- unset = options.unset;
- silent = options.silent;
- changes = [];
- changing = this._changing;
- this._changing = true;
+ //
+ var separator = DeepModel.keyPathSeparator;
+ var alreadyTriggered = {}; // * @restorer
- if (!changing) {
- this._previousAttributes = _.deepClone(this.attributes); //: Replaced _.clone with _.deepClone
- this.changed = {};
- }
- current = this.attributes, prev = this._previousAttributes;
-
- // Check for changes of `id`.
- if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
-
- //
- attrs = objToPaths(attrs);
- //
-
- // For each `set` attribute, update or delete the current value.
- for (attr in attrs) {
- val = attrs[attr];
-
- //: Using getNested, setNested and deleteNested
- if (!_.isEqual(getNested(current, attr), val)) changes.push(attr);
- if (!_.isEqual(getNested(prev, attr), val)) {
- setNested(this.changed, attr, val);
- } else {
- deleteNested(this.changed, attr);
- }
- unset ? deleteNested(current, attr) : setNested(current, attr, val);
- //
- }
+ for (var i = 0, l = changes.length; i < l; i++) {
+ var key = changes[i];
- // Trigger all relevant attribute changes.
- if (!silent) {
- if (changes.length) this._pending = true;
-
- //
- var separator = DeepModel.keyPathSeparator;
- var alreadyTriggered = {}; // * @restorer
-
- for (var i = 0, l = changes.length; i < l; i++) {
- var key = changes[i];
-
- if (!alreadyTriggered.hasOwnProperty(key) || !alreadyTriggered[key]) { // * @restorer
- alreadyTriggered[key] = true; // * @restorer
- this.trigger('change:' + key, this, getNested(current, key), options);
- } // * @restorer
-
- var fields = key.split(separator);
-
- //Trigger change events for parent keys with wildcard (*) notation
- for(var n = fields.length - 1; n > 0; n--) {
- var parentKey = _.first(fields, n).join(separator),
- wildcardKey = parentKey + separator + '*';
-
- if (!alreadyTriggered.hasOwnProperty(wildcardKey) || !alreadyTriggered[wildcardKey]) { // * @restorer
- alreadyTriggered[wildcardKey] = true; // * @restorer
- this.trigger('change:' + wildcardKey, this, getNested(current, parentKey), options);
- } // * @restorer
-
- // + @restorer
- if (!alreadyTriggered.hasOwnProperty(parentKey) || !alreadyTriggered[parentKey]) {
- alreadyTriggered[parentKey] = true;
- this.trigger('change:' + parentKey, this, getNested(current, parentKey), options);
- }
- // - @restorer
- }
- //
- }
- }
+ if (!alreadyTriggered.hasOwnProperty(key) || !alreadyTriggered[key]) { // * @restorer
+ alreadyTriggered[key] = true; // * @restorer
+ this.trigger('change:' + key, this, getNested(current, key), options);
+ } // * @restorer
- if (changing) return this;
- if (!silent) {
- while (this._pending) {
- this._pending = false;
- this.trigger('change', this, options);
- }
- }
- this._pending = false;
- this._changing = false;
- return this;
- },
-
- // Clear all attributes on the model, firing `"change"` unless you choose
- // to silence it.
- clear: function(options) {
- var attrs = {};
- var shallowAttributes = objToPaths(this.attributes);
- for (var key in shallowAttributes) attrs[key] = void 0;
- return this.set(attrs, _.extend({}, options, {unset: true}));
- },
-
- // Determine if the model has changed since the last `"change"` event.
- // If you specify an attribute name, determine if that attribute has changed.
- hasChanged: function(attr) {
- if (attr == null) return !_.isEmpty(this.changed);
- return getNested(this.changed, attr) !== undefined;
- },
-
- // Return an object containing all the attributes that have changed, or
- // false if there are no changed attributes. Useful for determining what
- // parts of a view need to be updated and/or what attributes need to be
- // persisted to the server. Unset attributes will be set to undefined.
- // You can also pass an attributes object to diff against the model,
- // determining if there *would be* a change.
- changedAttributes: function(diff) {
- //: objToPaths
- if (!diff) return this.hasChanged() ? objToPaths(this.changed) : false;
- //
+ var fields = key.split(separator);
- var old = this._changing ? this._previousAttributes : this.attributes;
+ //Trigger change events for parent keys with wildcard (*) notation
+ for (var n = fields.length - 1; n > 0; n--) {
+ var parentKey = _.first(fields, n).join(separator),
+ wildcardKey = parentKey + separator + '*';
- //
- diff = objToPaths(diff);
- old = objToPaths(old);
- //
+ if (!alreadyTriggered.hasOwnProperty(wildcardKey) || !alreadyTriggered[wildcardKey]) { // * @restorer
+ alreadyTriggered[wildcardKey] = true; // * @restorer
+ this.trigger('change:' + wildcardKey, this, getNested(current, parentKey), options);
+ } // * @restorer
- var val, changed = false;
- for (var attr in diff) {
- if (_.isEqual(old[attr], (val = diff[attr]))) continue;
- (changed || (changed = {}))[attr] = val;
+ // + @restorer
+ if (!alreadyTriggered.hasOwnProperty(parentKey) || !alreadyTriggered[parentKey]) {
+ alreadyTriggered[parentKey] = true;
+ this.trigger('change:' + parentKey, this, getNested(current, parentKey), options);
+ }
+ // - @restorer
}
- return changed;
- },
-
- // Get the previous value of an attribute, recorded at the time the last
- // `"change"` event was fired.
- previous: function(attr) {
- if (attr == null || !this._previousAttributes) return null;
-
- //
- return getNested(this._previousAttributes, attr);
//
- },
+ }
+ }
- // Get all of the attributes of the model at the time of the previous
- // `"change"` event.
- previousAttributes: function() {
- //
- return _.deepClone(this._previousAttributes);
- //
+ if (changing) return this;
+ if (!silent) {
+ while (this._pending) {
+ this._pending = false;
+ this.trigger('change', this, options);
}
- });
+ }
+ this._pending = false;
+ this._changing = false;
+ return this;
+ },
+
+ // Clear all attributes on the model, firing `"change"` unless you choose
+ // to silence it.
+ clear: function (options) {
+ var attrs = {};
+ var shallowAttributes = objToPaths(this.attributes);
+ for (var key in shallowAttributes) attrs[key] = void 0;
+ return this.set(attrs, _.extend({}, options, { unset: true }));
+ },
+
+ // Determine if the model has changed since the last `"change"` event.
+ // If you specify an attribute name, determine if that attribute has changed.
+ hasChanged: function (attr) {
+ if (attr == null) return !_.isEmpty(this.changed);
+ return getNested(this.changed, attr) !== undefined;
+ },
+
+ // Return an object containing all the attributes that have changed, or
+ // false if there are no changed attributes. Useful for determining what
+ // parts of a view need to be updated and/or what attributes need to be
+ // persisted to the server. Unset attributes will be set to undefined.
+ // You can also pass an attributes object to diff against the model,
+ // determining if there *would be* a change.
+ changedAttributes: function (diff) {
+ //: objToPaths
+ if (!diff) return this.hasChanged() ? objToPaths(this.changed) : false;
+ //
+
+ var old = this._changing ? this._previousAttributes : this.attributes;
+
+ //
+ diff = objToPaths(diff);
+ old = objToPaths(old);
+ //
+
+ var val, changed = false;
+ for (var attr in diff) {
+ if (_.isEqual(old[attr], (val = diff[attr]))) continue;
+ (changed || (changed = {}))[attr] = val;
+ }
+ return changed;
+ },
+
+ // Get the previous value of an attribute, recorded at the time the last
+ // `"change"` event was fired.
+ previous: function (attr) {
+ if (attr == null || !this._previousAttributes) return null;
+
+ //
+ return getNested(this._previousAttributes, attr);
+ //
+ },
+
+ // Get all of the attributes of the model at the time of the previous
+ // `"change"` event.
+ previousAttributes: function () {
+ //
+ return _.deepClone(this._previousAttributes);
+ //
+ }
+ });
- //Config; override in your app to customise
- DeepModel.keyPathSeparator = '.';
+ //Config; override in your app to customise
+ DeepModel.keyPathSeparator = '.';
- //Exports
- Backbone.DeepModel = DeepModel;
+ //Exports
+ Backbone.DeepModel = DeepModel;
- //For use in NodeJS
- if (typeof module != 'undefined') module.exports = DeepModel;
+ //For use in NodeJS
+ if (typeof module != 'undefined') module.exports = DeepModel;
- return Backbone;
+ return Backbone;
}));