Skip to content
Full library shelves on the left and right trailing off into the distance

Displaying Promises in EmberJS Templates

Brewer/developer/juggler Ewen Cumming on how to use promises in EmberJS templates.

Displaying Asynchronous Data in Templates

In Ember, a situation we commonly run into is needing to display asynchronous data in a template. A lot of the time this works really well, for example: Ember has some smarts around displaying related models within templates.

The below example shows a simple library application displaying authors and their published books:

 

import DS from 'ember-data';

export default DS.Model.extend({
 title: DS.attr('string'),
 year: DS.attr('number'),
 author: DS.belongsTo('author')
});
import DS from 'ember-data';

export default DS.Model.extend({
 name: DS.attr('string'),
 books: DS.hasMany('book')
});
import Ember from 'ember';

export default Ember.Route.extend({
 model () {
   return this.get('store').findAll('author');
 }
});
<h2>Authors</h2>

<ul>
{{#each model as |author|}}
 <li>{{author.name}}</li>
 <ul>
   {{#each author.books as |book|}}
     <li>{{book.title}} ({{book.year}})</li>
   {{/each}}
 </ul>
{{/each}}
</ul>
List of authors (Ursula K. Le Guin, Frederik Pohl, Neal Stephenson, Vernor Vinge)  and their published books

As you can see, despite the fact that getting the books is an asynchronous traversal of a relationship, the template renders correctly once the books have been fetched.

This is all well and good, but when you want to display some asynchronous data that isn’t just properties on a related model it gets more difficult.

For example, what if we wanted to asterisk authors that had published in the last two years? This requires us to get the related books and run a little logic. This seems like a useful function to have on our model so we can reuse it throughout our application.

import DS from 'ember-data';

export default DS.Model.extend({
 name: DS.attr('string'),
 books: DS.hasMany('book'),
 // Have any of their books been published in the last two years.
 hasPublishedRecently () {
   return this.get('books').then(books => {
     const currentYear = new Date().getFullYear();
     const years = books.map(b => b.get('year'));
     return years.some(y => y >= currentYear - 2);
   });
 }
});

However, now we can no longer use that value directly in the template – in fact, it requires some restructuring of our application. We could possibly pass in the resolved promise as part of our route model, however this will be clunky as we have a list of authors. Another approach is to create a separate component that is responsible for displaying the author. This component could call hasPublishedRecently  and set a property on the component once the promise resolves.

import Ember from 'ember';

export default Ember.Component.extend({
 didReceiveAttrs () {
   this._super(...arguments);
   this.get('author').hasPublishedRecently().then(result => {
     this.set('hasPublishedRecently', result);
   });
 },
 hasPublishedRecently: false
});
{{author.name}}{{if hasPublishedRecently '*'}}
<h2>Authors</h2>

<ul>
{{#each model as |author|}}
 <li>{{display-author author=author}}</li>
 <ul>
   {{#each author.books as |book|}}
     <li>{{book.title}} ({{book.year}})</li>
   {{/each}}
 </ul>
{{/each}}
</ul>

<p><em>* Published Recently</em></p>

This works, but requires us to add a new component as well as a chunk of extra code just to display the resolved promise value.

Using DS Promises

Ember does provide another way to do this. Rather than a function on the model we can use a computed property and if it returns a special type of object it can be used directly in the template. Ember Data provides these two classes:

  • DS.PromiseArray
  • DS.PromiseObject

They use the PromiseProxyMixin to give the Ember objects extra properties and methods that the templates can work with.

To demonstrate, we can adapt the previous example to use a computed property and return a PromiseObject  instance instead of a plain promise.

import DS from 'ember-data';
import Ember from 'ember';

export default DS.Model.extend({
 name: DS.attr('string'),
 books: DS.hasMany('book'),
 // Have any of their books been published in the last two years.
 hasPublishedRecently: Ember.computed('books', function () {
   const promise = this.get('books').then(books => {
     const currentYear = new Date().getFullYear();
     const years = books.map(b => b.get('year'));
     return years.some(y => y >= currentYear - 2);
   });
   return DS.PromiseObject.create({promise});
 })
});

Now we can use it directly in our template:

<h2>Authors</h2>
<ul>
{{#each model as |author|}}
 <li>{{author.name}}{{if author.hasPublishedRecently.content '*'}}</li>
 <ul>
   {{#each author.books as |book|}}
     <li>{{book.title}} ({{book.year}})</li>
   {{/each}}
 </ul>
{{/each}}
</ul>

<p><em>* Published Recently</em></p>
List of authors (Ursula K. Le Guin, Frederik Pohl, Neal Stephenson, Vernor Vinge)  and their published books, including and indicator of those recently published

Notice we now need to use the content  property of the object to display it in the template? If you want to ensure that the promise has resolved you can also use the properties isPending  or isSettled . See the PromiseProxyMixin for others.

For example, if we had a promise telling us if the book was available or on loan, we could wrap it in an unless isPending  block so nothing will show until the result was known:

<li>
 {{book.title}} ({{book.year}})
 {{#unless book.isAvailable.isPending}}
   - {{if book.isAvailable.content 'available' 'on loan'}}
 {{/unless}}
</li>

Drawbacks of DS Promises

Forgetting to use content or isPending properties

One drawback of this approach is that you now need awareness of whether you are dealing with a promise or a value in your templates. One convention we’ve used in our projects is giving the computed properties resolving to promises a Promise  suffix.

import DS from 'ember-data';
import Ember from 'ember';

export default DS.Model.extend({
 name: DS.attr('string'),
 books: DS.hasMany('book'),
 // Have any of their books been published in the last two years.
 hasPublishedRecentlyPromise: Ember.computed('books', function () {
   const promise = this.get('books').then(books => {
     const currentYear = new Date().getFullYear();
     const years = books.map(b => b.get('year'));
     return years.some(y => y >= currentYear - 2);
   });
   return DS.PromiseObject.create({promise});
 })
});

This adds a few more keystrokes but does make it much clearer in templates when dealing with these properties.

Boilerplate

This approach still requires some boilerplate code. It needs the promise to be wrapped in a DS.PromiseObject.create()  call and accessing the content property in the template. It may be possible to remove this with a helper that can take a regular promise and abstract away the details of PromiseObject  and optionally wrap the template code within an unless isPending  block.

Although we haven’t tried it, this library also looks promising and solves the same problem.

References

To see a working example of this code, you can check out this repository.

Media Suite
is now
MadeCurious.

All things change, and we change with them. But we're still here to help you build the right thing.

If you came looking for Media Suite, you've found us, we are now MadeCurious.

Media Suite MadeCurious.