3
votes

I'm trying to make a call to an external API and use the results as a computed property in my Ember Data model. The result is fetched fine, but the computed property returns before the Promise resolves, resulting in undefined. Is this a use case for an Observer?

export default DS.Model.extend({
  lat: DS.attr(),
  lng: DS.attr(),
  address: Ember.computed('lat', 'lng', function() {
    var url = `http://foo.com/json?param=${this.get('lat')},${this.get('lng')}`;
    var addr;

    var request = new Ember.RSVP.Promise(function(resolve, reject) {             
        Ember.$.ajax(url, {                                                        
          success: function(response) {                                            
            resolve(response);                                                     
          },                                                                       
          error: function(reason) {                                                
            reject(reason);                                                        
          }                                                                        
        });                                                                        
     });                                                                          

     request.then(function(response) {                      
       addr = response.results[0].formatted_address;                              
     }, function(error) {                                                         
       console.log(error);
     })  

     return addr;
  })
});
4
Sorry, I've deleted my answers as they do not work.. :( (Please see this for more info) discuss.emberjs.com/t/promises-and-computed-properties/3333] - Steve H.
A better technique would be to use a component to display the address, given lat and lng as input. It can fire the request in its init method, and set the display property in the success function. - Steve H.
Thanks for your help Steve! The above link provided an answer that works. I like this better than the component solution as it keeps the logic contained inside of the model. - codyjroberts
Glad you got it working, sorry for the false starts! - Steve H.
And welcome to StackOverflow! - Steve H.

4 Answers

5
votes

Use DS.PromiseObject. I use the following technique all the time:

import DS from 'ember-data';

export default DS.Model.extend({

  ...

  address: Ember.computed('lat', 'lng', function() {
    var request = new Ember.RSVP.Promise(function(resolve, reject) {             
      ...
    });                                                                          

    return DS.PromiseObject.create({ promise: request });
  }),

});

Use the resolved value in your templates as {{address.content}}, which will automatically update when the proxied Promise resolves.

If you want to do more here I'd recommend checking out what other people in the community are doing: https://emberobserver.com/?query=promise

It's not too hard to build a simple Component that accepts a DS.PromiseObject and show a loading spinner while the Promise is still pending, then shows the actual value (or yields to a block) once the Promise resolves.

I have an Ember.Service in the app I work on that's composed almost entirely of Computed Properties that return Promises wrapped in DS.PromiseObjects. It works surprisingly seamlessly.

1
votes

I've used the self.set('computed_property', value); technique in a large Ember application for about three months and I can tell you it have a very big problem: the computed property will only work once.

When you set the computed property value, the function that generated the result is lost, therefore when your related model properties change the computed property will not refresh.

Using promises inside computed properties in Ember is a hassle, the best technique I found is:

prop: Ember.computed('related', {
    // `get` receives `key` as a parameter but I never use it.
    get() {
        var self = this;
        // We don't want to return old values.
        this.set('prop', undefined);
            promise.then(function (value) {
                // This will raise the `set` method.
                self.set('prop', value);
            });
        // We're returning `prop_data`, not just `prop`.
        return this.get('prop_data');
    },
    set(key, value) {
        this.set('prop_data', value);
        return value;
    }
}),

Pros:

  • It work on templates, so you can do {{object.prop}} in a template and it will resolve properly.
  • It does update when the related properties change.

Cons:

  • When you do in Javascript object.get('prop'); and the promise is resolving, it will return you inmediately undefined, however if you're observing the computed property, the observer will fire again when the promise resolves and the final value is set.

Maybe you're wondering why I didn't returned the promise in the get; if you do that and use it in a template, it will render an object string representation ([object Object] or something like that).

I want to work in a proper computed property implementation that works well in templates, return a promise in Javascript and gets updated automatically, probably using something like DS.PromiseObject or Ember.PromiseProxyMixin, but unfortunately I didn't find time for it.

If the big con is not a problem for your use case use the "get/set" technique, if not try to implement a better method, but seriously do not just use self.set('prop', value);, it will give your a lot of problems in the long-term, it's not worth it.

PS.: The real, final solution for this problem, however, is: never use promises in computed properties if you can avoid it.

PS.: By the way, this technique isn't really mine but of my ex co-worker @reset-reboot.

0
votes

Create a component (address-display.js):

import Ember from 'ember';

export default Ember.Component.extend({
  init() {
    var url = `http://foo.com/json?param=${this.get('lat')},${this.get('lng')}`;
    Ember.$.ajax(url, {
      success: function(response) {
        this.set('value', response.results[0].formatted_address);
      },
      error: function(reason) {
        console.log(reason);
      }
    });
  }
});

Template (components/address-display.hbs):

{{value}}

Then use the component in your template:

{{address-display lat=model.lat lng=model.lng}}
-1
votes

The below works by resolving inside the property and setting the result.

Explained here: http://discuss.emberjs.com/t/promises-and-computed-properties/3333/10

export default DS.Model.extend({
  lat: DS.attr(),
  lng: DS.attr(),
  address: Ember.computed('lat', 'lng', function() {
    var url = `http://foo.com/json?param=${this.get('lat')},${this.get('lng')}`;
    var self = this;

    var request = new Ember.RSVP.Promise(function(resolve, reject) {             
        Ember.$.ajax(url, {                                                        
          success: function(response) {                                            
            resolve(response);                                                     
          },                                                                       
          error: function(reason) {                                                
            reject(reason);                                                        
          }                                                                        
        });                                                                        
     }).then(function(response) {
         self.set('address', response.results[0].formatted_address);
     })
  })
});