diff --git a/backbone-nested.js b/backbone-nested.js index ef1fbf9..d15b140 100644 --- a/backbone-nested.js +++ b/backbone-nested.js @@ -15,24 +15,11 @@ Backbone.NestedModel = Backbone.Model.extend({ get: function(attrStrOrPath){ - var attrPath = Backbone.NestedModel.attrPath(attrStrOrPath), - result; + var attrPath = Backbone.NestedModel.attrPath(attrStrOrPath); - Backbone.NestedModel.walkPath(this.attributes, attrPath, function(val, path){ - var attr = _.last(path); - if (path.length === attrPath.length){ - // attribute found - result = val[attr]; - } - }); - - return result; - }, - - has: function(attr){ - // for some reason this is not how Backbone.Model is implemented - it accesses the attributes object directly - var result = this.get(attr); - return !(result === null || _.isUndefined(result)); + return _.reduce(attrPath, function(val, attr) { + return _.isObject(val) ? val[attr] : undefined; + }, this.attributes); }, set: function(key, value, opts){ @@ -115,7 +102,7 @@ if (_.isObject(val)) { // clear child attrs setChanged(val, changedPath); } - if (!options.silent) model._delayedChange(changedPath, null); + model._delayedChange(changedPath, null); changed[changedPath] = null; }); }; @@ -124,11 +111,16 @@ this.attributes = {}; this._escapedAttributes = {}; + this._delayedTrigger('change'); + // Fire the `"change"` events. - if (!options.silent) this._delayedTrigger('change'); + if (!options.silent) this._runDelayedTriggers(); + return this; + }, + change: function() { this._runDelayedTriggers(); - return this; + return Backbone.NestedModel.__super__.change.apply(this, _.toArray(arguments)); }, add: function(attrStr, value, opts){ @@ -175,7 +167,6 @@ return Backbone.NestedModel.deepClone(this.attributes); }, - // private _delayedTrigger: function(/* the trigger args */){ _delayedTriggers.push(arguments); @@ -253,7 +244,7 @@ } } - if (!opts.silent){ + if (!opts.silent) { // let the superclass handle change events for top-level attributes if (path.length > 1 && isNewValue){ model._delayedChange(attrStr, val[attr]); @@ -263,6 +254,7 @@ model._delayedTrigger('add:' + attrStr, model, val[attr]); } } + }); } diff --git a/test/nested-model.js b/test/nested-model.js index 8ce94c3..5c7f220 100644 --- a/test/nested-model.js +++ b/test/nested-model.js @@ -48,7 +48,7 @@ $(document).ready(function() { test("#get() 1-1 returns attributes object", function() { var name = doc.get('name'); - + deepEqual(name, { first: 'Aidan', middle: { @@ -322,7 +322,7 @@ $(document).ready(function() { var changeNameFirst = sinon.spy(); var changeNameLast = sinon.spy(); var changeGender = sinon.spy(); - + doc.bind('change', change); doc.bind('change:name', changeName); doc.bind('change:name.first', changeNameFirst); @@ -356,6 +356,23 @@ $(document).ready(function() { sinon.assert.notCalled(changeNameFirst); }); + test("change() fires the silenced events", function() { + var change = sinon.spy(); + var changeName = sinon.spy(); + var changeNameFirst = sinon.spy(); + + doc.bind('change', change); + doc.bind('change:name', changeName); + doc.bind('change:name.first', changeNameFirst); + + doc.set({'name.first': 'Bob'}, {silent: true}); + doc.change(); + + sinon.assert.calledOnce(change); + sinon.assert.calledOnce(changeName); + sinon.assert.calledOnce(changeNameFirst); + }); + test("change event doesn't fire if new value matches old value", function() { var change = sinon.spy(); var changeName = sinon.spy(); @@ -504,7 +521,7 @@ $(document).ready(function() { var changeNameMiddleInitial = sinon.spy(); var changeNameMiddleFull = sinon.spy(); var changeNameFirst = sinon.spy(); - + doc.bind('change', change); doc.bind('change:name', changeName); doc.bind('change:name.middle', changeNameMiddle); @@ -536,12 +553,12 @@ $(document).ready(function() { var changeAddresses0City = sinon.spy(); var changeAddresses0State = sinon.spy(); var changeAddresses1 = sinon.spy(); - + doc.bind('change', change); doc.bind('change:addresses', changeAddresses); doc.bind('change:addresses[0]', changeAddresses0); doc.bind('change:addresses[0].city', changeAddresses0City); - + doc.bind('change:addresses[0].state', changeAddresses0State); doc.bind('change:addresses[1]', changeAddresses1); @@ -563,7 +580,7 @@ $(document).ready(function() { var change = sinon.spy(); var changeAddresses = sinon.spy(); var addAddresses = sinon.spy(); - + doc.bind('change', change); doc.bind('change:addresses', changeAddresses); doc.bind('add:addresses', addAddresses); @@ -585,7 +602,7 @@ $(document).ready(function() { var change = sinon.spy(); var changeAddresses = sinon.spy(); var removeAddresses = sinon.spy(); - + doc.bind('change', change); doc.bind('change:addresses', changeAddresses); doc.bind('remove:addresses', removeAddresses); @@ -601,7 +618,7 @@ $(document).ready(function() { var change = sinon.spy(); var changeAddresses = sinon.spy(); var removeAddresses = sinon.spy(); - + doc.bind('change', change); doc.bind('change:addresses', changeAddresses); doc.bind('remove:addresses', removeAddresses); @@ -625,7 +642,7 @@ $(document).ready(function() { full: 'Limburger', fullAlternates: ['Danger', 'Funny', 'Responsible'] }); - + doc.bind('change', change); doc.bind('change:name', changeName); doc.bind('change:name.middle', changeNameMiddle); @@ -662,7 +679,7 @@ $(document).ready(function() { 'name.middle.full': 'Limburger' }); }); - + doc.set({'name.middle.full': 'Limburger'}); }); @@ -684,7 +701,7 @@ $(document).ready(function() { 'name.middle.full': 'Limburger' }); }); - + //Set using conventional JSON - emulates a model fetch doc.set({ gender: 'M', @@ -708,7 +725,7 @@ $(document).ready(function() { ] }); }); - + test("#changedAttributes() should clear the nested attributes between change events", function() { doc.set({'name.first': 'Bob'}); @@ -735,7 +752,7 @@ $(document).ready(function() { return "First name is too long"; } }; - + doc.set({'name.first': 'TooLongFirstName'}); doc.bind('change', function(){ @@ -819,6 +836,23 @@ $(document).ready(function() { sinon.assert.notCalled(changeNameFirst); }); + test("#clear() silent triggers change events when change() is called", function() { + var change = sinon.spy(); + var changeName = sinon.spy(); + var changeNameFirst = sinon.spy(); + + doc.bind('change', change); + doc.bind('change:name', changeName); + doc.bind('change:name.first', changeNameFirst); + + doc.clear({silent: true}); + doc.change(); + + sinon.assert.calledOnce(change); + sinon.assert.calledOnce(changeName); + sinon.assert.calledOnce(changeNameFirst); + }); + // ----- UNSET -------- @@ -957,7 +991,7 @@ $(document).ready(function() { doc.bind('remove:addresses', removeAddresses); doc.remove('addresses[0]'); - + sinon.assert.calledOnce(removeAddresses); equal(doc.get('addresses').length, 1);