diff --git a/packages/ember-data/lib/system/associations/ext.js b/packages/ember-data/lib/system/associations/ext.js index f9b5075f261..ceb5d9cad08 100644 --- a/packages/ember-data/lib/system/associations/ext.js +++ b/packages/ember-data/lib/system/associations/ext.js @@ -39,6 +39,11 @@ DS.Model.reopen({ Ember.addBeforeObserver(proto, key, null, 'belongsToWillChange'); } + if (meta.isAttribute) { + Ember.addObserver(proto, key, null, 'attributeDidChange'); + Ember.addBeforeObserver(proto, key, null, 'attributeWillChange'); + } + meta.parentType = proto.constructor; } } diff --git a/packages/ember-data/lib/system/model/attributes.js b/packages/ember-data/lib/system/model/attributes.js index 93fa63e2a90..7b5ca217d42 100644 --- a/packages/ember-data/lib/system/model/attributes.js +++ b/packages/ember-data/lib/system/model/attributes.js @@ -19,12 +19,47 @@ DS.Model.reopenClass({ }) }); +var AttributeChange = DS.AttributeChange = function(options) { + this.reference = options.reference; + this.store = options.store; + this.name = options.name; + this.oldValue = options.oldValue; +}; + +AttributeChange.createChange = function(options) { + return new AttributeChange(options); +}; + +AttributeChange.prototype = { + sync: function() { + this.store.recordAttributeDidChange(this.reference, this.name, this.value, this.oldValue); + + // TODO: Use this object in the commit process + this.destroy(); + }, + + destroy: function() { + delete this.store.recordForReference(this.reference)._changesToSync[this.name]; + } +}; + DS.Model.reopen({ eachAttribute: function(callback, binding) { get(this.constructor, 'attributes').forEach(function(name, meta) { callback.call(binding, name, meta); }, binding); - } + }, + + attributeWillChange: Ember.beforeObserver(function(record, key) { + var reference = get(record, 'reference'), + store = get(record, 'store'); + + record.send('willSetProperty', { reference: reference, store: store, name: key }); + }), + + attributeDidChange: Ember.observer(function(record, key) { + record.send('didSetProperty', { name: key }); + }) }); function getAttr(record, options, key) { @@ -51,10 +86,7 @@ DS.attr = function(type, options) { var data; if (arguments.length > 1) { - // TODO: If there is a cached oldValue, use it [tomhuda] - oldValue = get(this, 'data.attributes')[key]; - Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('')` from " + this.toString(), key !== 'id'); - this.setProperty(key, value, oldValue); + Ember.assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('')` from " + this.constructor.toString(), key !== 'id'); } else { value = getAttr(this, options, key); } diff --git a/packages/ember-data/lib/system/model/model.js b/packages/ember-data/lib/system/model/model.js index b9acc47811a..e480a06bfa8 100644 --- a/packages/ember-data/lib/system/model/model.js +++ b/packages/ember-data/lib/system/model/model.js @@ -54,6 +54,8 @@ DS.Model = Ember.Object.extend(Ember.Evented, { }).property(), materializeData: function() { + this.send('materializingData'); + get(this, 'store').materializeData(this); this.suspendAssociationObservers(function() { @@ -160,6 +162,8 @@ DS.Model = Ember.Object.extend(Ember.Evented, { this.hasManyDidChange(association.key); } }, this); + + this.send('finishedMaterializing'); }, 'data'), hasManyDidChange: function(key) { @@ -189,8 +193,6 @@ DS.Model = Ember.Object.extend(Ember.Evented, { hasMany: {}, id: null }; - - this.notifyPropertyChange('data'); }, materializeId: function(id) { diff --git a/packages/ember-data/lib/system/model/states.js b/packages/ember-data/lib/system/model/states.js index a6d6cc32784..fa6953146d3 100644 --- a/packages/ember-data/lib/system/model/states.js +++ b/packages/ember-data/lib/system/model/states.js @@ -180,14 +180,17 @@ var didChangeData = function(manager) { record.materializeData(); }; -var setProperty = function(manager, context) { - var record = get(manager, 'record'), - store = get(record, 'store'), - key = context.key, - oldValue = context.oldValue, - newValue = context.value; - - store.recordAttributeDidChange(record, key, newValue, oldValue); +var willSetProperty = function(manager, context) { + context.oldValue = get(get(manager, 'record'), context.name); + + var change = DS.AttributeChange.createChange(context); + get(manager, 'record')._changesToSync[context.attributeName] = change; +}; + +var didSetProperty = function(manager, context) { + var change = get(manager, 'record')._changesToSync[context.attributeName]; + change.value = get(get(manager, 'record'), context.name); + change.sync(); }; // Whenever a property is set, recompute all dependent filters @@ -278,7 +281,8 @@ var DirtyState = DS.State.extend({ }, // EVENTS - setProperty: setProperty, + willSetProperty: willSetProperty, + didSetProperty: didSetProperty, becomeDirty: Ember.K, @@ -294,7 +298,7 @@ var DirtyState = DS.State.extend({ t.recordBecameClean(dirtyType, record); }); - manager.transitionTo('loaded.saved'); + manager.transitionTo('loaded.materializing'); }, becameInvalid: function(manager) { @@ -381,10 +385,12 @@ var DirtyState = DS.State.extend({ get(manager, 'record').clearRelationships(); }, - setProperty: function(manager, context) { + willSetProperty: willSetProperty, + + didSetProperty: function(manager, context) { var record = get(manager, 'record'), errors = get(record, 'errors'), - key = context.key; + key = context.name; set(errors, key, null); @@ -392,7 +398,7 @@ var DirtyState = DS.State.extend({ manager.send('becameValid'); } - setProperty(manager, context); + didSetProperty(manager, context); }, becomeDirty: Ember.K, @@ -497,19 +503,11 @@ var states = { // Usually, this process is asynchronous, using an // XHR to retrieve the data. loading: DS.State.create({ - // TRANSITIONS - exit: function(manager) { - var record = get(manager, 'record'); - - Ember.run.once(function() { - record.trigger('didLoad'); - }); - }, - // EVENTS - loadedData: function(manager) { - didChangeData(manager); - manager.transitionTo('loaded'); + loadedData: didChangeData, + + materializingData: function(manager) { + manager.transitionTo('loaded.materializing.firstTime'); } }), @@ -524,15 +522,46 @@ var states = { // SUBSTATES + materializing: DS.State.create({ + // FLAGS + isLoaded: false, + + // EVENTS + willSetProperty: Ember.K, + didSetProperty: Ember.K, + + didChangeData: didChangeData, + + finishedMaterializing: function(manager) { + manager.transitionTo('loaded.saved'); + }, + + // SUBSTATES + firstTime: DS.State.create({ + exit: function(manager) { + var record = get(manager, 'record'); + + Ember.run.once(function() { + record.trigger('didLoad'); + }); + } + }) + }), + // If there are no local changes to a record, it remains // in the `saved` state. saved: DS.State.create({ - // EVENTS - setProperty: setProperty, + willSetProperty: willSetProperty, + didSetProperty: didSetProperty, + didChangeData: didChangeData, loadedData: didChangeData, + materializingData: function(manager) { + manager.transitionTo('loaded.materializing'); + }, + becomeDirty: function(manager) { manager.transitionTo('updated'); }, @@ -647,7 +676,7 @@ var states = { t.recordBecameClean('deleted', record); }); - manager.transitionTo('loaded.saved'); + manager.transitionTo('loaded.materializing'); } }), diff --git a/packages/ember-data/lib/system/store.js b/packages/ember-data/lib/system/store.js index cd1a7e8cc8e..6ad0cb45245 100644 --- a/packages/ember-data/lib/system/store.js +++ b/packages/ember-data/lib/system/store.js @@ -353,6 +353,10 @@ DS.Store = Ember.Object.extend(DS._Mappable, { // the `loaded` state. record.loadedData(); + // Make sure the data is set up so the record doesn't + // try to materialize its nonexistent data. + record.setupData(); + // Store the record we just created in the record cache for // this clientId. this.recordCache[clientId] = record; @@ -1759,8 +1763,9 @@ DS.Store = Ember.Object.extend(DS._Mappable, { // . RECORD CHANGE NOTIFICATION . // .............................. - recordAttributeDidChange: function(record, attributeName, newValue, oldValue) { - var dirtySet = new Ember.OrderedSet(), + recordAttributeDidChange: function(reference, attributeName, newValue, oldValue) { + var record = this.recordForReference(reference), + dirtySet = new Ember.OrderedSet(), adapter = this.adapterForType(record.constructor); if (adapter.dirtyRecordsForAttributeChange) { diff --git a/packages/ember-data/tests/integration/record_persistence_test.js b/packages/ember-data/tests/integration/record_persistence_test.js index 13542c0df8a..4e1d461f407 100644 --- a/packages/ember-data/tests/integration/record_persistence_test.js +++ b/packages/ember-data/tests/integration/record_persistence_test.js @@ -328,9 +328,9 @@ test("An error is raised when attempting to set a property while a record is bei try { tom.set('name', "Tommy Bahama"); } catch(e) { - var expectedMessage = "Attempted to handle event `setProperty` on "; + var expectedMessage = "Attempted to handle event `willSetProperty` on "; expectedMessage += "while in state rootState.loaded.updated.inFlight. Called with "; - expectedMessage += "{key: name , value: Tommy Bahama , oldValue: null}"; + expectedMessage += "{reference: [object Object] , store: , name: name}"; equal(e.message, expectedMessage); } finishSaving(); diff --git a/packages/ember-data/tests/unit/fixture_adapter_test.js b/packages/ember-data/tests/unit/fixture_adapter_test.js index b253fda50a3..a745531d2f2 100644 --- a/packages/ember-data/tests/unit/fixture_adapter_test.js +++ b/packages/ember-data/tests/unit/fixture_adapter_test.js @@ -46,7 +46,7 @@ test("should load data for a type asynchronously when it is requested", function equal(get(ebryn, 'isLoaded'), false, "record from fixtures is returned in the loading state"); - ebryn.addObserver('isLoaded', function() { + ebryn.on('didLoad', function() { clearTimeout(timer); start(); @@ -56,7 +56,7 @@ test("should load data for a type asynchronously when it is requested", function stop(); var wycats = store.find(Person, 'wycats'); - wycats.addObserver('isLoaded', function() { + wycats.on('didLoad', function() { clearTimeout(timer); start();