diff --git a/distribution/deep-model.js b/distribution/deep-model.js index 5307ffc..cf8453d 100644 --- a/distribution/deep-model.js +++ b/distribution/deep-model.js @@ -9,7 +9,17 @@ * Licensed under the MIT License */ -/** +;(function(factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define(['underscore', 'backbone'], factory); + } else { + // globals + factory(_, Backbone); + } +}(function(_, Backbone) { + + /** * Underscore mixins for deep objects * * Based on https://gist.github.com/echong/3861963 @@ -120,319 +130,310 @@ }).call(this); /** - * Main source +* Main source +*/ + + + +/** + * 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; -;(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; + 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; } - - 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.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]]; + return ret; +} - if (result == null && i < n - 1) { - result = {}; - } - - if (typeof result === 'undefined') { - if (return_exists) - { - return true; - } - return result; - } +/** + * @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.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; } - if (return_exists) - { - return true; + result = result[fields[i]]; + + if (result == null && i < n - 1) { + result = {}; } - 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.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]; + if (typeof result === 'undefined') { + if (return_exists) + { + return true; } + return result; } } - - function deleteNested(obj, path) { - setNested(obj, path, null, { unset: true }); + if (return_exists) + { + return true; } + return result; +} - 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 = {}; +/** + * @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] = {}; } - current = this.attributes, prev = this._previousAttributes; - - // Check for changes of `id`. - if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + //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')) { // - attrs = objToPaths(attrs); + // 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; + } - // 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); - // - } - - // Trigger all relevant attribute changes. - if (!silent) { - if (changes.length) this._pending = true; + options || (options = {}); - // - var separator = DeepModel.keyPathSeparator; + // Run validation. + if (!this._validate(attrs, options)) return false; - for (var i = 0, l = changes.length; i < l; i++) { - var key = changes[i]; + // Extract attributes and options. + unset = options.unset; + silent = options.silent; + changes = []; + changing = this._changing; + this._changing = true; - this.trigger('change:' + key, this, getNested(current, key), options); + if (!changing) { + this._previousAttributes = _.deepClone(this.attributes); //: Replaced _.clone with _.deepClone + this.changed = {}; + } + current = this.attributes, prev = this._previousAttributes; - var fields = key.split(separator); + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; - //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 + '*'; + // + attrs = objToPaths(attrs); + // - this.trigger('change:' + wildcardKey, this, getNested(current, parentKey), options); - } - // - } - } + // For each `set` attribute, update or delete the current value. + for (attr in attrs) { + val = attrs[attr]; - 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; + //: 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); // + } + + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = true; - var old = this._changing ? this._previousAttributes : this.attributes; - // - diff = objToPaths(diff); - old = objToPaths(old); - // + var separator = DeepModel.keyPathSeparator; - var val, changed = false; - for (var attr in diff) { - if (_.isEqual(old[attr], (val = diff[attr]))) continue; - (changed || (changed = {}))[attr] = val; - } - return changed; - }, + for (var i = 0, l = changes.length; i < l; i++) { + var key = changes[i]; - // 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; + this.trigger('change:' + key, this, getNested(current, key), options); - // - return getNested(this._previousAttributes, attr); - // - }, + var fields = key.split(separator); - // Get all of the attributes of the model at the time of the previous - // `"change"` event. - previousAttributes: function() { - // - return _.deepClone(this._previousAttributes); - // + //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 + '*'; + + this.trigger('change:' + wildcardKey, this, getNested(current, parentKey), options); + } + // + } } - }); + 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; - //For use in NodeJS - if (typeof module != 'undefined') module.exports = DeepModel; - - return Backbone; +//Exports +Backbone.DeepModel = DeepModel; +//For use in NodeJS +if (typeof module != 'undefined') module.exports = DeepModel; + + return Backbone; })); diff --git a/distribution/deep-model.min.js b/distribution/deep-model.min.js index e488ad5..a87a946 100644 --- a/distribution/deep-model.min.js +++ b/distribution/deep-model.min.js @@ -9,4 +9,18 @@ * 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(factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define(['underscore', 'backbone'], factory); + } else { + // globals + factory(_, Backbone); + } +}(function(_, Backbone) { + + (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)t=o(t,i(r.shift()),n);return t},e.mixin({deepClone:i,isBasicObject:u,basicObjects:r,arrays:n,deepExtend:s})}).call(this);var o=t.Model.extend({constructor:function(t,n){var r,i=t||{};this.cid=e.uniqueId("c"),this.attributes={},n&&n.collection&&(this.collection=n.collection),n&&n.parse&&(i=this.parse(i,n)||{});if(r=e.result(this,"defaults"))i=e.deepExtend({},r,i);this.set(i,n),this.changed={},this.initialize.apply(this,arguments)},toJSON:function(t){return e.deepClone(this.attributes)},get:function(e){return r(this.attributes,e)},set:function(t,u,a){var f,l,c,h,p,d,v,m;if(t==null)return this;typeof t=="object"?(l=t,a=u||{}):(l={})[t]=u,a||(a={});if(!this._validate(l,a))return!1;c=a.unset,p=a.silent,h=[],d=this._changing,this._changing=!0,d||(this._previousAttributes=e.deepClone(this.attributes),this.changed={}),m=this.attributes,v=this._previousAttributes,this.idAttribute in l&&(this.id=l[this.idAttribute]),l=n(l);for(f in l)u=l[f],e.isEqual(r(m,f),u)||h.push(f),e.isEqual(r(v,f),u)?s(this.changed,f):i(this.changed,f,u),c?s(m,f):i(m,f,u);if(!p){h.length&&(this._pending=!0);var g=o.keyPathSeparator;for(var y=0,b=h.length;y0;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}) + + return Backbone; +})); + diff --git a/scripts/template.js b/scripts/template.js index 95fe0f4..bf47c14 100644 --- a/scripts/template.js +++ b/scripts/template.js @@ -9,4 +9,18 @@ * Licensed under the MIT License */ -{{body}} +;(function(factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define(['underscore', 'backbone'], factory); + } else { + // globals + factory(_, Backbone); + } +}(function(_, Backbone) { + + {{body}} + + return Backbone; +})); + diff --git a/src/deep-model.js b/src/deep-model.js index 55c19dd..807e6bc 100644 --- a/src/deep-model.js +++ b/src/deep-model.js @@ -1,316 +1,304 @@ /** - * Main source +* Main source +*/ + + + +/** + * 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; -;(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; + 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; } - - 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.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]]; + return ret; +} - if (result == null && i < n - 1) { - result = {}; - } - - if (typeof result === 'undefined') { - if (return_exists) - { - return true; - } - return result; - } +/** + * @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.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; } - if (return_exists) - { - return true; + result = result[fields[i]]; + + if (result == null && i < n - 1) { + result = {}; } - 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.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]; + if (typeof result === 'undefined') { + if (return_exists) + { + return true; } + return result; } } - - function deleteNested(obj, path) { - setNested(obj, path, null, { unset: true }); + if (return_exists) + { + return true; } + return result; +} - 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 = {}; +/** + * @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] = {}; } - current = this.attributes, prev = this._previousAttributes; - - // Check for changes of `id`. - if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + //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')) { // - attrs = objToPaths(attrs); + // 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; + } - // 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); - // - } - - // Trigger all relevant attribute changes. - if (!silent) { - if (changes.length) this._pending = true; + options || (options = {}); - // - var separator = DeepModel.keyPathSeparator; + // Run validation. + if (!this._validate(attrs, options)) return false; - for (var i = 0, l = changes.length; i < l; i++) { - var key = changes[i]; + // Extract attributes and options. + unset = options.unset; + silent = options.silent; + changes = []; + changing = this._changing; + this._changing = true; - this.trigger('change:' + key, this, getNested(current, key), options); + if (!changing) { + this._previousAttributes = _.deepClone(this.attributes); //: Replaced _.clone with _.deepClone + this.changed = {}; + } + current = this.attributes, prev = this._previousAttributes; - var fields = key.split(separator); + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; - //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 + '*'; + // + attrs = objToPaths(attrs); + // - this.trigger('change:' + wildcardKey, this, getNested(current, parentKey), options); - } - // - } - } + // For each `set` attribute, update or delete the current value. + for (attr in attrs) { + val = attrs[attr]; - 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; + //: 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); // + } + + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = true; - var old = this._changing ? this._previousAttributes : this.attributes; - // - diff = objToPaths(diff); - old = objToPaths(old); - // + var separator = DeepModel.keyPathSeparator; - var val, changed = false; - for (var attr in diff) { - if (_.isEqual(old[attr], (val = diff[attr]))) continue; - (changed || (changed = {}))[attr] = val; - } - return changed; - }, + for (var i = 0, l = changes.length; i < l; i++) { + var key = changes[i]; - // 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; + this.trigger('change:' + key, this, getNested(current, key), options); - // - return getNested(this._previousAttributes, attr); - // - }, + var fields = key.split(separator); - // Get all of the attributes of the model at the time of the previous - // `"change"` event. - previousAttributes: function() { - // - return _.deepClone(this._previousAttributes); - // + //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 + '*'; + + this.trigger('change:' + wildcardKey, this, getNested(current, parentKey), options); + } + // + } } - }); + 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; - //For use in NodeJS - if (typeof module != 'undefined') module.exports = DeepModel; - - return Backbone; +//Exports +Backbone.DeepModel = DeepModel; -})); +//For use in NodeJS +if (typeof module != 'undefined') module.exports = DeepModel; \ No newline at end of file