angular2-promise-buttons
Version:
Chilled loading buttons for angular
262 lines (255 loc) • 9.87 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, Directive, Inject, Input, HostListener, NgModule } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
const DEFAULT_CFG = {
spinnerTpl: '<span class="btn-spinner"></span>',
disableBtn: true,
btnLoadingClass: 'is-loading',
handleCurrentBtnOnly: false,
minDuration: null,
};
const userCfg = new InjectionToken('cfg');
class PromiseBtnDirective {
constructor(el, cfg) {
// provide configuration
this.cfg = Object.assign({}, DEFAULT_CFG, cfg);
// save element
this.btnEl = el.nativeElement;
}
// this is added to fix the overriding of the disabled state by the loading indicator button.
// https://github.com/johannesjo/angular2-promise-buttons/issues/34
set isDisabledFromTheOutsideSetter(v) {
this.isDisabledFromTheOutside = v;
if (v) {
// disabled means always disabled
this.btnEl.setAttribute('disabled', 'disabled');
}
else if (this.isPromiseDone || this.isPromiseDone === undefined) {
this.btnEl.removeAttribute('disabled');
}
// else the button is loading, so do not change the disabled loading state.
}
set promiseBtn(passedValue) {
const isObservable = passedValue instanceof Observable;
const isSubscription = passedValue instanceof Subscription;
const isBoolean = typeof passedValue === 'boolean';
const isPromise = passedValue instanceof Promise || (passedValue !== null &&
typeof passedValue === 'object' &&
typeof passedValue.then === 'function' &&
typeof passedValue.catch === 'function');
if (isObservable) {
throw new TypeError('promiseBtn must be an instance of Subscription, instance of Observable given');
}
else if (isSubscription) {
const sub = passedValue;
if (!sub.closed) {
this.promise = new Promise((resolve) => {
sub.add(resolve);
});
}
}
else if (isPromise) {
this.promise = passedValue;
}
else if (isBoolean) {
this.promise = this.createPromiseFromBoolean(passedValue);
}
this.checkAndInitPromiseHandler(this.btnEl);
}
ngAfterContentInit() {
this.prepareBtnEl(this.btnEl);
// trigger changes once to handle initial promises
this.checkAndInitPromiseHandler(this.btnEl);
}
ngOnDestroy() {
// cleanup
if (this.minDurationTimeout) {
clearTimeout(this.minDurationTimeout);
}
}
createPromiseFromBoolean(val) {
if (val) {
return new Promise((resolve) => {
this._fakePromiseResolve = resolve;
});
}
else {
if (this._fakePromiseResolve) {
this._fakePromiseResolve();
}
return this.promise;
}
}
/**
* Initializes all html and event handlers
*/
prepareBtnEl(btnEl) {
// handle promises passed via promiseBtn attribute
this.appendSpinnerTpl(btnEl);
}
/**
* Checks if all required parameters are there and inits the promise handler
*/
checkAndInitPromiseHandler(btnEl) {
// check if element and promise is set
if (btnEl && this.promise) {
this.initPromiseHandler(btnEl);
}
}
/**
* Helper FN to add class
*/
addLoadingClass(el) {
if (typeof this.cfg.btnLoadingClass === 'string') {
el.classList.add(this.cfg.btnLoadingClass);
}
}
/**
* Helper FN to remove classes
*/
removeLoadingClass(el) {
if (typeof this.cfg.btnLoadingClass === 'string') {
el.classList.remove(this.cfg.btnLoadingClass);
}
}
/**
* Handles everything to be triggered when the button is set
* to loading state.
*/
initLoadingState(btnEl) {
this.addLoadingClass(btnEl);
this.disableBtn(btnEl);
}
/**
* Handles everything to be triggered when loading is finished
*/
cancelLoadingStateIfPromiseAndMinDurationDone(btnEl) {
if ((!this.cfg.minDuration || this.isMinDurationTimeoutDone) && this.isPromiseDone) {
this.removeLoadingClass(btnEl);
this.enableBtn(btnEl);
}
}
disableBtn(btnEl) {
if (this.cfg.disableBtn) {
btnEl.setAttribute('disabled', 'disabled');
}
}
enableBtn(btnEl) {
if (this.cfg.disableBtn) {
if (this.isDisabledFromTheOutside) {
btnEl.setAttribute('disabled', 'disabled');
}
else {
btnEl.removeAttribute('disabled');
}
}
}
/**
* Initializes a watcher for the promise. Also takes
* this.cfg.minDuration into account if given.
*/
initPromiseHandler(btnEl) {
const promise = this.promise;
// watch promise to resolve or fail
this.isMinDurationTimeoutDone = false;
this.isPromiseDone = false;
// create timeout if option is set
if (this.cfg.minDuration) {
this.minDurationTimeout = window.setTimeout(() => {
this.isMinDurationTimeoutDone = true;
this.cancelLoadingStateIfPromiseAndMinDurationDone(btnEl);
}, this.cfg.minDuration);
}
const resolveLoadingState = () => {
this.isPromiseDone = true;
this.cancelLoadingStateIfPromiseAndMinDurationDone(btnEl);
};
if (!this.cfg.handleCurrentBtnOnly) {
this.initLoadingState(btnEl);
}
// native Promise doesn't have finally
if (promise.finally) {
promise.finally(resolveLoadingState);
}
else {
promise
.then(resolveLoadingState)
.catch(resolveLoadingState);
}
}
/**
* $compile and append the spinner template to the button.
*/
appendSpinnerTpl(btnEl) {
// TODO add some kind of compilation later on
btnEl.insertAdjacentHTML('beforeend', this.cfg.spinnerTpl);
}
/**
* Limit loading state to show only for the currently clicked button.
* Executed only if this.cfg.handleCurrentBtnOnly is set
*/
handleCurrentBtnOnly() {
if (!this.cfg.handleCurrentBtnOnly) {
return true; // return true for testing
}
// Click triggers @Input update
// We need to use timeout to wait for @Input to update
window.setTimeout(() => {
// return if something else than a promise is passed
if (!this.promise) {
return;
}
this.initLoadingState(this.btnEl);
}, 0);
}
}
PromiseBtnDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.17", ngImport: i0, type: PromiseBtnDirective, deps: [{ token: i0.ElementRef }, { token: userCfg }], target: i0.ɵɵFactoryTarget.Directive });
PromiseBtnDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "12.2.17", type: PromiseBtnDirective, selector: "[promiseBtn]", inputs: { isDisabledFromTheOutsideSetter: ["disabled", "isDisabledFromTheOutsideSetter"], promiseBtn: "promiseBtn" }, host: { listeners: { "click": "handleCurrentBtnOnly()" } }, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.17", ngImport: i0, type: PromiseBtnDirective, decorators: [{
type: Directive,
args: [{
selector: '[promiseBtn]'
}]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: undefined, decorators: [{
type: Inject,
args: [userCfg]
}] }]; }, propDecorators: { isDisabledFromTheOutsideSetter: [{
type: Input,
args: ['disabled']
}], promiseBtn: [{
type: Input
}], handleCurrentBtnOnly: [{
type: HostListener,
args: ['click']
}] } });
class Angular2PromiseButtonModule {
// add forRoot to make it configurable
static forRoot(config) {
// NOTE: this is never allowed to contain any conditional logic
return {
ngModule: Angular2PromiseButtonModule,
providers: [{ provide: userCfg, useValue: config }]
};
}
}
Angular2PromiseButtonModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.17", ngImport: i0, type: Angular2PromiseButtonModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
Angular2PromiseButtonModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "12.2.17", ngImport: i0, type: Angular2PromiseButtonModule, declarations: [PromiseBtnDirective], exports: [PromiseBtnDirective] });
Angular2PromiseButtonModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "12.2.17", ngImport: i0, type: Angular2PromiseButtonModule, providers: [], imports: [[]] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.17", ngImport: i0, type: Angular2PromiseButtonModule, decorators: [{
type: NgModule,
args: [{
declarations: [
PromiseBtnDirective,
],
imports: [],
exports: [
PromiseBtnDirective,
],
providers: []
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { Angular2PromiseButtonModule, PromiseBtnDirective };
//# sourceMappingURL=angular2-promise-buttons.js.map