diff --git a/src/app/HISTORY.md b/src/app/HISTORY.md index 1e88e10315e..1e6298d2f28 100644 --- a/src/app/HISTORY.md +++ b/src/app/HISTORY.md @@ -4,6 +4,16 @@ App Framework Change History 3.5.0 ----- +### Model + +* `load()` now fires a `load` event after the operation completes successfully, + or an `error` event on failure. The `load()` callback (if provided) will still + be called in both cases. [Ticket #2531207] + +* `save()` now fires a `save` event after the operation completes successfully, + or an `error` event on failure. The `save()` callback (if provided) will still + be called in both cases. [Ticket #2531207] + ### ModelList * Added a `filter()` method that returns a filtered array of models. [Ticket diff --git a/src/app/docs/model/index.mustache b/src/app/docs/model/index.mustache index 0c17aeabebf..f6dd2fde9db 100644 --- a/src/app/docs/model/index.mustache +++ b/src/app/docs/model/index.mustache @@ -272,28 +272,28 @@ Model instances provide the following events:
-
changed (Object)
+
`changed` (Object)

Hash of change information for each attribute that changed. Keys are attribute names, values are objects with the following properties:

-
`newVal`
+
`newVal`

The new value of the attribute after it changed.

-
`prevVal`
+
`prevVal`

The old value of the attribute before it changed.

-
`src`
+
`src`

The source of the change, or `null` if no source was specified when the change was made. @@ -318,28 +318,42 @@ Model instances provide the following events:

-
error
+
`error`

Error message, object, or exception generated by the error. Calling `toString()` on this should result in a meaningful error message.

-
src
+
`src`

Source of the error. May be one of the following default sources, or any custom error source used by your Model subclass):

-
`parse`
+
`load`
+
+

+ An error loading the model from a sync layer. The sync layer's response (if any) will be provided as the `response` property on the event facade. +

+
+ +
`parse`

An error parsing a response from a sync layer.

-
`validate`
+
`save`
+
+

+ An error saving the model to a sync layer. The sync layer's response (if any) will be provided as the `response` property on the event facade. +

+
+ +
`validate`

The model failed to validate. @@ -350,6 +364,58 @@ Model instances provide the following events:

+ + + `load` + +

+ After model attributes are loaded from a sync layer. +

+ + +
+
`parsed`
+
+

+ The parsed version of the sync layer's response to the load request. +

+
+ +
`response`
+
+

+ The sync layer's raw, unparsed response to the load request. +

+
+
+ + + + + `save` + +

+ After model attributes are saved to a sync layer. +

+ + +
+
`parsed`
+
+

+ The parsed version of the sync layer's response to the save request. +

+
+ +
`response`
+
+

+ The sync layer's raw, unparsed response to the save request. +

+
+
+ + @@ -659,6 +725,10 @@ pie.save(function (err, response) { }); ``` +

+In addition to calling the specified callback (if any), the `load()` and `save()` methods will fire a `load` event and a `save` event respectively on success, or an `error` event on failure. See [[#Model Events]] for more details on these events. +

+

Always use the `load()` or `save()` methods rather than calling `sync()` directly, since this ensures that the sync layer's response is passed through the `parse()` method and that the model's data is updated if necessary.

diff --git a/src/app/js/model.js b/src/app/js/model.js index 11565a8d5bd..158431192af 100644 --- a/src/app/js/model.js +++ b/src/app/js/model.js @@ -49,12 +49,45 @@ var GlobalEnv = YUI.namespace('Env.Model'), @param {String} src Source of the error. May be one of the following (or any custom error source defined by a Model subclass): + * `load`: An error loading the model from a sync layer. The sync layer's + response (if any) will be provided as the `response` property on the + event facade. + * `parse`: An error parsing a JSON response. The response in question will be provided as the `response` property on the event facade. + + * `save`: An error saving the model to a sync layer. The sync layer's + response (if any) will be provided as the `response` property on the + event facade. + * `validate`: The model failed to validate. The attributes being validated will be provided as the `attributes` property on the event facade. **/ - EVT_ERROR = 'error'; + EVT_ERROR = 'error', + + /** + Fired after model attributes are loaded from a sync layer. + + @event load + @param {Object} parsed The parsed version of the sync layer's response to + the load request. + @param {any} response The sync layer's raw, unparsed response to the load + request. + @since 3.5.0 + **/ + EVT_LOAD = 'load', + + /** + Fired after model attributes are saved to a sync layer. + + @event save + @param {Object} [parsed] The parsed version of the sync layer's response to + the save request, if there was a response. + @param {any} [response] The sync layer's raw, unparsed response to the save + request, if there was one. + @since 3.5.0 + **/ + EVT_SAVE = 'save'; function Model() { Model.superclass.constructor.apply(this, arguments); @@ -299,6 +332,9 @@ Y.Model = Y.extend(Model, Y.Base, { operation, which is an asynchronous action. Specify a _callback_ function to be notified of success or failure. + A successful load operation will fire a `load` event, while an unsuccessful + load operation will fire an `error` event with the `src` value "load". + If the load operation succeeds and one or more of the loaded attributes differ from this model's current attributes, a `change` event will be fired. @@ -324,16 +360,41 @@ Y.Model = Y.extend(Model, Y.Base, { options = {}; } - this.sync('read', options, function (err, response) { - if (!err) { - self.setAttrs(self.parse(response), options); + options || (options = {}); + + self.sync('read', options, function (err, response) { + var facade = { + options : options, + response: response + }, + + parsed; + + if (err) { + facade.error = err; + facade.src = 'load'; + + self.fire(EVT_ERROR, facade); + } else { + // Lazy publish. + if (!self._loadEvent) { + self._loadEvent = self.publish(EVT_LOAD, { + preventable: false + }); + } + + parsed = facade.parsed = self.parse(response); + + self.setAttrs(parsed, options); self.changed = {}; + + self.fire(EVT_LOAD, facade); } callback && callback.apply(null, arguments); }); - return this; + return self; }, /** @@ -378,6 +439,9 @@ Y.Model = Y.extend(Model, Y.Base, { operation, which is an asynchronous action. Specify a _callback_ function to be notified of success or failure. + A successful load operation will fire a `load` event, while an unsuccessful + load operation will fire an `error` event with the `src` value "load". + If the save operation succeeds and one or more of the attributes returned in the server's response differ from this model's current attributes, a `change` event will be fired. @@ -410,13 +474,36 @@ Y.Model = Y.extend(Model, Y.Base, { return self; } + options || (options = {}); + self.sync(self.isNew() ? 'create' : 'update', options, function (err, response) { - if (!err) { + var facade = { + options : options, + response: response + }, + + parsed; + + if (err) { + facade.error = err; + facade.src = 'save'; + + self.fire(EVT_ERROR, facade); + } else { + // Lazy publish. + if (!self._loadEvent) { + self._loadEvent = self.publish(EVT_LOAD, { + preventable: false + }); + } + if (response) { - self.setAttrs(self.parse(response), options); + parsed = facade.parsed = self.parse(response); + self.setAttrs(parsed, options); } self.changed = {}; + self.fire(EVT_SAVE, facade); } callback && callback.apply(null, arguments); @@ -543,7 +630,7 @@ Y.Model = Y.extend(Model, Y.Base, { @param {Object} [options] Sync options. It's up to the custom sync implementation to determine what options it supports or requires, if any. - @param {callback} [callback] Called when the sync operation finishes. + @param {Function} [callback] Called when the sync operation finishes. @param {Error|null} callback.err If an error occurred, this parameter will contain the error. If the sync operation succeeded, _err_ will be falsy. diff --git a/src/app/tests/app-test.js b/src/app/tests/app-test.js index 41c2f52384e..76a3ace1c30 100644 --- a/src/app/tests/app-test.js +++ b/src/app/tests/app-test.js @@ -702,6 +702,100 @@ modelSuite.add(new Y.Test.Case({ model.parse('moo'); Assert.areSame(1, calls); + }, + + '`error` event should fire when a load operation fails': function () { + var calls = 0, + model = new this.TestModel(); + + model.on('error', function (e) { + calls += 1; + + Assert.areSame('load', e.src); + Assert.areSame('foo', e.error); + Assert.areSame('{"error": true}', e.response); + Assert.isObject(e.options); + }); + + model.sync = function (action, options, callback) { + callback('foo', '{"error": true}'); + }; + + model.load(); + + Assert.areSame(1, calls); + }, + + '`error` event should fire when a save operation fails': function () { + var calls = 0, + model = new this.TestModel(); + + model.on('error', function (e) { + calls += 1; + + Assert.areSame('save', e.src); + Assert.areSame('foo', e.error); + Assert.areSame('{"error": true}', e.response); + Assert.isObject(e.options); + }); + + model.sync = function (action, options, callback) { + callback('foo', '{"error": true}'); + }; + + model.save(); + + Assert.areSame(1, calls); + }, + + '`load` event should fire after a successful load operation': function () { + var calls = 0, + model = new this.TestModel(); + + model.on('load', function (e) { + calls += 1; + + Assert.areSame('{"foo": "bar"}', e.response); + Assert.isObject(e.options); + Assert.isObject(e.parsed); + Assert.areSame('bar', e.parsed.foo); + Assert.areSame('bar', model.get('foo'), 'load event should fire after attribute changes are applied'); + }); + + model.sync = function (action, options, callback) { + callback(null, '{"foo": "bar"}'); + }; + + model.load(function () { + Assert.areSame(1, calls, 'load event should fire before the callback runs'); + }); + + Assert.areSame(1, calls, 'load event never fired'); + }, + + '`save` event should fire after a successful save operation': function () { + var calls = 0, + model = new this.TestModel(); + + model.on('save', function (e) { + calls += 1; + + Assert.areSame('{"foo": "bar"}', e.response); + Assert.isObject(e.options); + Assert.isObject(e.parsed); + Assert.areSame('bar', e.parsed.foo); + Assert.areSame('bar', model.get('foo'), 'save event should fire after attribute changes are applied'); + }); + + model.sync = function (action, options, callback) { + callback(null, '{"foo": "bar"}'); + }; + + model.save(function () { + Assert.areSame(1, calls, 'save event should fire before the callback runs'); + }); + + Assert.areSame(1, calls, 'save event never fired'); } }));