5

In EmberData calling model.save() causes the model to be persisted via whatever adapter is in place. If the adapter returns some data (such as JSON from an API) the model is updated with that data.

I have stumbled upon a sequence where this is not true.

In a voucher system for a checkout process a voucherCode is entered on an order model. When an 'Apply' button is pressed the order is saved via order.save() and the voucher is thus submitted to the server.

If the voucher code is valid a voucherValue field is populated with a number. If the voucher code is invalid a 422 error is returned with a standard errors object as per http://emberjs.com/api/data/classes/DS.Errors.html

Now, here is where things go awry. If a code is entered that returns a voucherValue of 300 a controller property calculates the discount.

discount: function () {
  var discount = this.get('model.voucherValue');
  // some calculation
  return discount;
}.property('model.voucherValue')

If, for whatever reason, the user then enters an invalid code we return an error as described above. The server removes the discount and sets the voucherValue to 0

As the error response does not contain the updated data in the catch of the save we manually update it.

order.save().then(function () {

}).catch(function (error) {

  order.set('voucherValue', 0);
});

The discount computed property is updated as expected on setting the voucherValue. However, inspecting the order model shows that order._data.voucherValue is still the original 300 value from the first valid voucher code - as EmberData does not know this value is persisted on the server.

If we then enter a valid voucher code that returns a voucherValue of 300 (the same as it was originally) the discount computed property is not recalculated.

It appears Ember is checking the returned data values against order._data and as there is no difference is not triggering any property recalculations.

I've tried different workarounds but have been unable to find something that works reliably.

Frustratingly there does not seem to be a reliable way to access the returned data and manually set the voucherValue from the returned data. Even if the returned data sets a value for voucherValue the following is true:

order.save().then(function (savedOrder) {

  savedOrder.get('voucherValue') === 0; //true

}).catch(function (error) {
  order.set('voucherValue', 0);
});

However, if a different voucher is entered after an invalid voucher and the voucherValue is different (say 450) everything works as expected.

Is this a bug in EmberData? Is there a know workaround. I'm open to suggestions and willing to try anything before I try and reengineer how this entire system is implemented.

Binarytales
  • 9,468
  • 9
  • 32
  • 38
  • Please do change the wording of the title/question if you can, I know what I have put is pretty awful. – Binarytales Jun 01 '15 at 15:05
  • I'm not totally sure, but I think I once had this problem... I had to change the http response code to 204 No Content I guess... – enspandi Jun 01 '15 at 17:38
  • I'm unable to replicate your scenario.. You say that after trying to save a voucherValue of 300, getting an error from the server then setting voucherValue to 0 (in your catch), if you then set it to 300 again the computed property doesn't recalculate.. for me it does.. – jmurphyau Jun 04 '15 at 07:11
  • I've created a JSBin to help with this question.. http://emberjs.jsbin.com/guraxu/11/edit?js,output ... this demonstrates that setting a value (e.g. 300), then clicking 'save, fail and reset' attempts to save, fails with 422 and sets voucherValue to 0 - if you then set the value to 300 again, the discount value does actually get updated.. – jmurphyau Jun 04 '15 at 13:19

2 Answers2

0

This bit of your code seems to be a problem:

order.save().then(function (savedOrder) {
  savedOrder.get('voucherValue') === 0; //true
}).catch(function (error) {
  order.set('voucherValue', 0);
});

The catch function is a proxy to the promise's catch. However, promises normally expect a 2nd parameter to be passed to the then function, like this:

order.save().then(function (savedOrder) {
  savedOrder.get('voucherValue') === 0; //true
}, function (error) {
  order.set('voucherValue', 0);
});

This 2nd function represents the reject path of the resolve/reject pair used by the Promise. See: http://emberjs.com/api/data/classes/DS.Model.html#method_save

Steve H.
  • 6,912
  • 2
  • 30
  • 49
  • From my reading of the documentation for Promises using a `catch` is functionally equivalent to passing the `reject` handler as a second argument to `then` http://emberjs.com/api/classes/RSVP.Promise.html#method_catch – Binarytales Jun 03 '15 at 13:08
  • 1
    So did you try my suggestion or are you going strictly by your interpretation of the docs? – Steve H. Jun 03 '15 at 15:22
  • BTW, the "catch" function doc says: `catch is simply sugar for then(undefined, onRejection) which makes it the same as the catch block of a try/catch statement.` So you are chaining a `then()` to the existing `then()` if that makes sense. It is not functionally equivalent to passing the `reject` handler as a second argument to the first `then` – Steve H. Jun 03 '15 at 15:24
0

If the server is supposed to be setting model.voucherValue, then you need to stop setting it on the client. Setting it on the client can interfere with the record's intended state while Ember Data is trying to materialize it in the adapter.

I think it can be solved by changing your property definition a bit:

discount: function () {
  if (this.get('model.errors.length') > 0) {
    return 0;
  }
  var discount = this.get('model.voucherValue');
  // some calculation
  return discount;
}.property('model.voucherValue', 'model.errors.length')

Or something similar, depending on your use case.

mpowered
  • 13,162
  • 2
  • 15
  • 18
  • This is something I had considered but I haven't been able to see how to use Ember's built in error system *and* return the updated value (`voucherValue = 0`) in the server response. – Binarytales Jun 08 '15 at 14:47
  • If there's an error on the server, then you shouldn't be returning anything to the client aside from the error. The server's state shouldn't be pushed to the client until it becomes valid again--and you can't just make assumptions about what the values on the server will be, which is why I'm suggesting `discount` as an intermediate value that makes sense to the user. – mpowered Jun 08 '15 at 17:20