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"
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'
});
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'
});
And you can use it like so
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:
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:
- not support the state transitions when the button is used with named actions
- 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;
}
Everything herein is copyright 2015 James A Rosen, Fastly and available under the Apache Public License, v2.0