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