UNPKG

ember-bootstrap

Version:
459 lines (400 loc) 11.3 kB
import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import Component from '@glimmer/component'; import arg from '../utils/decorators/arg'; type ButtonState = 'default' | 'pending' | 'fulfilled' | 'rejected'; interface ButtonSignature<VALUE = undefined> { Element: HTMLButtonElement; Args: { active?: boolean; block?: boolean; bubble?: boolean; buttonType?: 'button' | 'submit' | 'reset'; defaultText?: string; fulfilledText?: string; icon?: string; iconActive?: string; iconInactive?: string; outline?: boolean; pendingText?: string; preventConcurrency?: boolean; rejectedText?: string; reset?: boolean; size?: 'lg' | 'sm' | null; type?: string; value?: VALUE; onClick?: (value?: VALUE) => Promise<void> | void; // private args, for internal use only! /** * @internal */ _disabled?: boolean; /** * @internal */ state?: ButtonState; /** * @internal */ attrTypePrivateWorkaround?: 'button' | 'submit'; }; Blocks: { default: [ { isFulfilled: boolean; isPending: boolean; isRejected: boolean; isSettled: boolean; }, ]; }; } /** Implements a HTML button element, with support for all [Bootstrap button CSS styles](http://getbootstrap.com/css/#buttons) as well as advanced functionality such as button states. ### Basic Usage ```hbs <BsButton @type="primary" @icon="glyphicon glyphicon-download"> Downloads </BsButton> ``` ### Actions Use the `onClick` property of the component to send an action to your controller. It will receive the button's value (see the `value` property) as an argument. ```hbs <BsButton @type="primary" @icon="glyphicon glyphicon-download" @onClick={{this.download}}> Downloads </BsButton> ``` ### Promise support for automatic state change When returning a Promise for any asynchronous operation from the `onClick` closure action the button will manage an internal state ("default" > "pending" > "fulfilled"/"rejected") automatically. The button is disabled by default if it's in pending state. You could override this behavior by passing the `disabled` HTML attribute or by setting `@preventConcurrency` to false. ```hbs <BsButton disabled={{false}} /> ``` ```hbs <BsButton @preventConcurrency={{false}} /> ``` The label could be changed automatically according to the state of the promise with `@defaultText`, `@pendingText`, `@fulfilledText` and `@rejectedText` arguments: ```hbs <BsButton @type="primary" @icon="glyphicon glyphicon-download" @defaultText="Download" @pendingText="Loading..." @fulfilledText="Completed!" @rejectedText="Oups!?" @onClick={{this.download}} /> ``` ```js // controller.js import { Controller } from '@ember/controller'; import { action } from '@ember/object'; export default class MyController extends Controller { @action download() { return new Promise(...); } } ``` For further customization `isPending`, `isFulfilled`, `isRejected` and `isSettled` properties are yielded: ```hbs <BsButton @onClick={{this.download}} as |button|> Download {{#if button.isPending}} <span class="loading-spinner"></span> {{/if}} </BsButton> ``` You can `reset` the state represented by these properties and used for button's text by setting `reset` property to `true`. *Note that only invoking the component in a template as shown above is considered part of its public API. Extending from it (subclassing) is generally not supported, and may break at any time.* @class Button @namespace Components @extends Glimmer.Component @public */ export default class Button<VALUE> extends Component<ButtonSignature<VALUE>> { /** * Default label of the button. Not need if used as a block component * * @property defaultText * @type string * @public */ /** * Label of the button used if `onClick` event has returned a Promise which is pending. * Not considered if used as a block component. * * @property pendingText * @type string * @public */ /** * Label of the button used if `onClick` event has returned a Promise which succeeded. * Not considered if used as a block component. * * @property fulfilledText * @type string * @public */ /** * Label of the button used if `onClick` event has returned a Promise which failed. * Not considered if used as a block component. * * @property rejectedText * @type string * @public */ /** * Property to disable the button only used in internal communication * between Ember Boostrap components. * * @property _disabled * @type ?boolean * @default null * @private */ get __disabled() { if (this.args._disabled !== undefined) { return this.args._disabled; } return this.isPending && this.args.preventConcurrency !== false; } /** * Set the 'active' class to apply active/pressed CSS styling * * @property active * @type boolean * @default false * @public */ /** * Property for block level buttons (BS3 and BS4 only!) * * See the [Bootstrap docs](http://getbootstrap.com/css/#buttons-sizes) * @property block * @type boolean * @default false * @public */ @arg block = false; /** * A click event on a button will not bubble up the DOM tree if it has an `onClick` action handler. Set to true to * enable the event to bubble * * @property bubble * @type boolean * @default false * @public */ /** * If button is active and this is set, the icon property will match this property * * @property iconActive * @type String * @public */ /** * If button is inactive and this is set, the icon property will match this property * * @property iconInactive * @type String * @public */ /** * Class(es) (e.g. glyphicons or font awesome) to use as a button icon * This will render a <i class="{{icon}}"></i> element in front of the button's label * * @property icon * @type String * @public */ get icon() { return ( this.args.icon || (this.args.active ? this.args.iconActive : this.args.iconInactive) ); } /** * Supply a value that will be associated with this button. This will be sent * as a parameter of the default action triggered when clicking the button * * @property value * @type any * @public */ /** * Controls if `onClick` action is fired concurrently. If `true` clicking button multiple times will not trigger * `onClick` action if a Promise returned by previous click is not settled yet. * * This does not affect event bubbling. * * @property preventConcurrency * @type Boolean * @default true * @public */ /** * State of the button. The button's label (if not used as a block component) will be set to the * `<state>Text` property. * This property will automatically be set when using a click action that supplies the callback with a promise. * Possible values are: "default" > "pending" > "fulfilled" / "rejected". * It could be resetted by `reset` property. * * @property state * @type String * @default 'default' * @private */ @tracked _state: ButtonState = 'default'; get state() { return this.args.state ?? this._state; } set state(state: ButtonState) { this._state = state; } /** * Promise returned by `onClick` event is pending. * * @property isPending * @type Boolean * @private */ get isPending() { return this.state === 'pending'; } /** * Promise returned by `onClick` event has been succeeded. * * @property isFulfilled * @type Boolean * @private */ get isFulfilled() { return this.state === 'fulfilled'; } /** * Promise returned by `onClick` event has been rejected. * * @property isRejected * @type Boolean * @private */ get isRejected() { return this.state === 'rejected'; } /** * Promise returned by `onClick` event has been succeeded or rejected. * * @property isSettled * @type Boolean * @private */ get isSettled() { return this.isFulfilled || this.isRejected; } /** * Set this to `true` to reset the `state`. A typical use case is to bind this attribute with ember-data isDirty flag. * * The old value is not taken into consideration. Setting a `true` value to `true` again will also reset `state`. * In that case it's even to notify the observer system that the property has changed by calling * [`notifyPropertyChange()`](https://www.emberjs.com/api/ember/3.2/classes/EmberObject/methods/notifyPropertyChange?anchor=notifyPropertyChange). * * @property reset * @type boolean * @public */ /** * Property for size styling, set to 'lg', 'sm' or 'xs' * * Also see the [Bootstrap docs](https://getbootstrap.com/docs/4.3/components/buttons/#sizes) * * @property size * @type String * @public */ /** * Property for type styling * * For the available types see the [Bootstrap docs](https://getbootstrap.com/docs/4.3/components/buttons/) * * @property type * @type String * @default 'secondary' * @public */ /** * Property to create outline buttons (BS4+ only) * * @property outline * @type boolean * @default false * @public */ /** * When clicking the button this action is called with the value of the button (that is the value of the "value" property). * * Return a promise object, and the buttons state will automatically set to "pending", "resolved" and/or "rejected". * This could be used to automatically set the button's text depending on promise state (`defaultText`, `pendingText`, * `fulfilledText`, `rejectedText`) and for further customization using the yielded `isPending`, `isFulfilled`, * `isRejected` properties. * * The click event will not bubble up, unless you set `bubble` to true. * * @event onClick * @param {*} value * @public */ /** * This will reset the state property to 'default', and with that the button's label to defaultText * * @method resetState * @private */ @action resetState() { this.state = 'default'; } get text() { return this.args[`${this.state}Text`] || this.args.defaultText; } /** * @method click * @private */ @action async handleClick(e: MouseEvent) { const { bubble, onClick, preventConcurrency } = this.args; if (typeof onClick !== 'function') { return; } // Shouldn't we prevent propagation regardless if `@onClick` is a function? if (!bubble) { e.stopPropagation(); } if (preventConcurrency && this.isPending) { return; } this.state = 'pending'; try { await onClick(this.args.value); if (!this.isDestroyed) { this.state = 'fulfilled'; } } catch (error) { if (!this.isDestroyed) { this.state = 'rejected'; } throw error; } } }