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.

User clicking

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 '@glimmer/component'
import { task } from 'ember-concurrency'

export default class SaveButtonComponent extends Component {
  @task
  *save() {
    yield this.model.save()
  }
}

Resulting in this

Ember concurrency task UI

Hold your clicks, 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.

Frustrated user

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 { tracked } from '@glimmer/tracking'
import { task, timeout } from 'ember-concurrency'

export default class SaveButtonComponent extends Component {
  @tracked timeLeft = 30;

  @task
  *save() {
    yield this.model.save()

    for (let index = this.timeLeft - 1; index >= 0; index--) {
      yield timeout(1000)
      this.timeLeft = index
    }

    this.timeLeft = timeLeft
  }
}

And here's what we get

Ember concurrency timer UI

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 '@glimmer/component'
import { tracked } from '@glimmer/tracking'
import { task, timeout } from 'ember-concurrency'

export default class SendCodeComponent extends Component {
  @tracked timeLeft = 30;

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

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

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

    this.timeLeft = timeLeft
  }
}

And the final result looks something like this

Ember concurrency promise-timer UI

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.

Subscribe below to get future posts from Sabin