Promises and UI states in Ember.js

Generally, when we deal with promises we need to be able to manipulate the UI, while a promise is pending. Think about showing a loading indicator, displaying a message on the screen or changing the state of a button. We do this to give feedback to the user about what is going on. Or sometimes we need to block the user to take certain actions, like clicking a button for example.

Let’s say we have a save button that will save a model and will persist that to the server. We need a way to disable the button while saving. This way we’re avoiding the user loosing their patience and clicking on it many times. The result can be sending many requests before the first one resolves.

Use the Task, Luke!

A great Ember add-on to deal with this type of situations is ember concurrency. If you want to learn more about how it works please read this article by Alex Matchneer, the creator of the addon. The idea is that we can write promise-based operations as generator functions. In ember-concurrency we do this using a primitive called Task that the add-on offers us. Tasks are nothing more than generator functions that use the yield operator.

Any ember concurrency task has a `isRunning` property. It is equal to true while the task is performing. Here’s how we can leverage this:

{{!-- template.hbs --}}

<button {{on "click" (perform this.save)}} disabled={{this.save.isRunning}}>
  {{if this.save.isRunning "Saving…" "Save"}}
</button>
//component.js

import Component from '@ember/component';
import classic from 'ember-classic-decorator';
import { task } from 'ember-concurrency';

@classic
class SaveButtonComponent extends Component {
  @task(function * () {
    yield this.model.save();
  }) save
}

export default SaveButtonComponent;

Resulting in this

Hold your clicks my dear user

One other example of what we can do leveraging ember concurrency is the following. Let’s say we have button that once clicked will send a request to a third party sms service like Twilio which in turn will send a verification code to the user’s phone. Usually we want to wait some time until the user clicks again the button as it can take a bit more until the sms arrives. Having the user clicking more than once can easily complicate the situation where multiple verification codes may arrive at almost the same time without knowing which is the one required now.

We can block the user clicking the button for a certain period of time while displaying a countdown until the button will be ready for clicking again. The way to go is leveraging the `timeout()` method from ember concurrency. Here’s how we can make this happen:

{{!-- template.hbs --}}

{{#if this.send.isRunning}}
  Send again in {{this.timeLeft}} seconds.
{{else}}
  <button {{on "click" (perform this.send)}}>
    Send sms code
  </button>
{{/if}}
// component.js

import Component from '@ember/component';
import classic from 'ember-classic-decorator';
import { task, timeout } from 'ember-concurrency';

@classic
class SaveButtonComponent extends Component {
  timeLeft = 30

  @task(function * () {
    let { timeLeft } = this

    yield this.model.save();
    
    for (let index = timeLeft - 1; index >= 0; index--) {
      yield timeout(1000);
      this.set('timeLeft', index);
    }

    this.set('timeLeft', timeLeft);
  }) send
}

export default SaveButtonComponent;

And here’s what we get

Tying everything together

Here’s what we can do if we want to go a step further and enhance the UX even more. We can disable the button while the promise is resolved (getting the response from the server) and once this is done we show the timer. Doing this is pretty simple with the ember concurrency’s child tasks. Here’s the code:

{{!-- template.hbs --}}

{{#if this.send.isRunning}}
  Send again in {{this.timeLeft}} seconds.
{{else}}
  <button {{on "click" (perform this.saveAndSend)}} disabled={{this.save.isRunning}}>
    {{if this.save.isRunning "Sending…" "Send sms code"}}
  </button>
{{/if}}
// component.js

import Component from '@ember/component';
import classic from 'ember-classic-decorator';
import { task, timeout } from 'ember-concurrency';

@classic
class SendCodeComponent extends Component {
  timeLeft = 30

  @task(function * () {
    yield this.save.perform();
    yield this.send.perform();
  }) saveAndSend

  @task(function * () {
    yield this.model.save();
  }) save

  @task(function * () {
    let { timeLeft } = this
    
    for (let index = timeLeft - 1; index >= 0; index--) {
      yield timeout(1000);
      this.set('timeLeft', index);
    }

    this.set('timeLeft', timeLeft);
  }) send
}

export default SaveButtonComponent;

And the final result looks something like this

Dealing with promises and handling UI states based on these is pretty painful. Luckily ember concurrency gives us tremendous power to make things work. What we’ve done here are just a few examples of what we can achieve. There are many other creative ways to use ember concurrency but we can explore these in future posts.

Looking for an Ember Job? Find your next adventure at emberwork.com

Leave a Reply

Your email address will not be published. Required fields are marked *