Skip to content

Instantly share code, notes, and snippets.

@jamesarosen
Last active October 25, 2015 16:41
Show Gist options
  • Save jamesarosen/3c54827b7016f0f19465 to your computer and use it in GitHub Desktop.
Save jamesarosen/3c54827b7016f0f19465 to your computer and use it in GitHub Desktop.
An Ember button with "waiting" and "done" states

I recently put together a button component. I looked through the various buttons in our codebase and came up with the following requirements:

  • understands being disabled
  • blockless- or block form
  • supports closure actions or named (route) actions
  • support for stateful lifecycle: "Save" "Saving..." "Saved"

Disabled

We start off super small. Adding a disabled attribute to the <button> tag is easy:

// my-button/component.js
export default Ember.Component.extend({
  attributeBindings: [ 'disabled' ],
  disabled: false,
  tagName: 'button'
});

Blockless and Block Form

Supporting both of these is fairly straightforward. We use positionalParams and pull off the first one.

// my-button/component.js
export default Ember.Component.extend({
  blocklessText: Ember.computed.reads('texts.0')
}).reopenClass({
  positionalParams: 'texts'
});
{{!-- my-button/template.hbs --}}
{{#if hasBlock}}
  {{yield}}
{{else}}
  {{blocklessText}}
{{/if}}

And you can use it like so

{{my-button 'Click Me'}}

{{#my-button}}
  {{my-icon 'trash'}}
  Delete
{{/my-button}}

Actions

Supporting both closure actions and traditional named ones is also fairly easy. We just check which one we have in the click handler.

click() {
  const action = this.get('action');
  if (Ember.isFunction(action)) {
    action(...arguments);
  } else if (action != null) {
    this.sendAction(action, ...arguments);
  }
}

Usage:

{{my-button 'Save' action='save'}}
{{my-button 'Delete' action=(action 'delete')}}

Asynchronous Statefulness

Here is the tricky part. We need to use a Promise to communicate flow from clicking to performing the asynchronous action to it resolving. sendAction doesn't let the receiver return a meaningful value to the component. So we have two options:

  1. not support the state transitions when the button is used with named actions
  2. work around this by having the button create a Deferred and pass it up to the action recipient

We decided that while (2) was too much of a design compromise. Instead, we just push ourselves to use more closure actions -- even if that means having a top-level component that does HTTP API access in an action.

To make our button support (1), we add inProgress and complete properties and a little Promise-handling logic to click:

complete: false,
inProgress: false,

click() {
  const action = this.get('action');
  let result;

  if (isFunction(action)) {
    result = action(...arguments);
  } else if (action != null) {
    this.sendAction(action, ...arguments);
  }

  if (result && isFunction(result.then)) {
    this.set('inProgress', true);
    this.set('complete', false);

    const onDone = () => {
      this.set('inProgress', false);
      this.set('complete', true);
    };

    result.then(onDone, onDone);
  }

  return result;
}

License

Everything herein is copyright 2015 James A Rosen, Fastly and available under the Apache Public License, v2.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment