Closure Actions and Promises

This article is a continuation of my previous one on Using Closure Actions in Ember.js. In the previous article, I did a brief overview on how actions have evolved in recent history, and how Ember's closure action helper in conjunction with the Dockyard add-on ember-route-action-helper can be utilized to build a hierarchy of actions that are also capable of returning values.

Being able to return values from the route level down to components can lead to some pretty powerful patterns, particularly with promises.

Returning promises

For example, lets look at an action that saves a user:

// app/users/route.js
export default Ember.Route.extend({  
  actions: {
    saveUser(user) {
      return user.save();
    }
  }
});

.save() will return a promise. In our component, when we execute saveUser, we can use this return value to our advantage:

// app/components/user-edit/component.js
export default Ember.Component.extend({  
  actions: {
    saveUser() {
      this.get('saveUser')().then(user => {
        this.set('successMsg', `User ${user.get('name')} saved`);
      });
    }
  }
});

In this code, the component defines a resolve clause for the promise returned by saveUser. On a succcessful save, the component will present UI that communicates that the save was successful.

What's nice about this is that data persistence is isolated at the route level, and UI concerns are isolated to the component.

Handling promises from actions gives us the ability to handle both failures and successes in a tidy way. It also can become pretty powerful when executing multiple actions that each return promises.

Chaining Promises and Actions

Let's say we wanted to transition after successfully saving the user. Let's also say that we have a new action on our route, called transitionToIndex, that we pass to our component. Our component-level action could look like this:

saveUser() {  
  this.get('saveUser')()
    .then(user => this.set('successMsg', `User ${user.get('name')} saved`))
    .then(() => this.get('transitionToIndex'))
    .then(() => { /* on successful transition */ });
  });
}

Allowing our actions to return promises gives us an opportunity to act on them with further action calls, eliciting smaller and finer-tuned action methods and more power in the component to decide when to call on these actions.

Handling Errors

You can handle errors at both the route-level and component-level.

// app/users/route.js
export default Ember.Route.extend({  
  actions: {
    saveUser(user) {
      return user.save()
        .catch(() => throw user.rollbackAttributes());
    }
  }
});
// app/components/user-edit/component.js
export default Ember.Component.extend({  
  actions: {
    saveUser() {
      this.get('saveUser')()
        .then(user => this.set('successMsg', `User ${user.get('name')} saved`))
        .catch(() => this.set('errorMessage', 'An error occurred'));
    }
  }
});

Not only will the route execute .rollbackAttributes() on the failed promise, but the reject clause at the component level will execute as well.

(When using catch be sure to throw otherwise it will fail silently and your subsequent then clauses will execute as if there wasn't an error.)

The possibilities are endless

Here is a more complicated example:

// app/users/route.js
export default Ember.Route.extend({  
  actions: {
    saveUser(user) {
      return user.save()
        .catch(() => { throw user.rollbackAttributes(); });
    },

    addUserToGroup(group, user) {
      return group.get('users').addObject(user).save()
        .catch(() => { throw group.rollbackAttributes(); });
    },

    transitionToIndex() {
      return this.transitionTo('index');
    },
  }
});
// app/components/user-edit/component.js
export default Ember.Component.extend({  
  actions: {
    saveUser() {
      this.get('saveUser')()
        .catch(() => { throw this.set('errorMsg', 'User not saved'); })
        .then(() => this.get('addUserToGroup')())
        .catch(() => { throw this.set('errorMsg', 'User not added to group'); });
        .then(() => this.get('transitionToIndex')()) 
    }
  }
});