UNPKG

ngx-suspense

Version:

This library is an experimetnal implementation of React Suspense for Angular.

431 lines (422 loc) 18.1 kB
import { __read, __decorate, __param, __spread, __values, __assign } from 'tslib'; import { InjectionToken, Optional, Inject, Injectable, Input, Component, ContentChildren, NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { BehaviorSubject, Subject, timer, combineLatest, merge, Observable, of } from 'rxjs'; import { switchMap, takeUntil, mapTo, startWith, skip, filter, tap, concatMap, shareReplay, pairwise } from 'rxjs/operators'; import { trigger, transition, style, animate } from '@angular/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { CommonModule } from '@angular/common'; var LOADING_DEFUALT_CONFIG = { busyMinDurationMs: 0, busyDelayMs: 0, }; var LOADING_CONFIG_TOKEN = new InjectionToken("LOADING_CONFIG_TOKEN"); var NgxSuspenseService = /** @class */ (function () { function NgxSuspenseService(userConfig) { if (userConfig === void 0) { userConfig = LOADING_DEFUALT_CONFIG; } this.userConfig = userConfig; this.loadingSubject = new BehaviorSubject(false); this.loading$ = this.loadingSubject.asObservable(); this.taskStartSubject = new Subject(); this.taskStart$ = this.taskStartSubject.asObservable(); this.taskEndSubject = new Subject(); this.taskEnd$ = this.taskEndSubject.asObservable(); } NgxSuspenseService.prototype.ngOnDestroy = function () { if (this.sub && typeof this.sub.unsubscribe === "function") { this.sub.unsubscribe(); } }; Object.defineProperty(NgxSuspenseService.prototype, "busyTimer", { set: function (_a) { var busyDelayMs = _a.busyDelayMs, busyMinDurationMs = _a.busyMinDurationMs; if (typeof busyDelayMs === "number") { this.userConfig.busyDelayMs = busyDelayMs; } if (typeof busyMinDurationMs === "number") { this.userConfig.busyMinDurationMs = busyMinDurationMs; } }, enumerable: true, configurable: true }); Object.defineProperty(NgxSuspenseService.prototype, "config", { get: function () { return this.userConfig; }, enumerable: true, configurable: true }); NgxSuspenseService.prototype.show = function () { this.loadingSubject.next(true); }; NgxSuspenseService.prototype.hide = function () { this.loadingSubject.next(false); }; NgxSuspenseService.prototype.controller = function () { var _this = this; this.busyMinDurationTimer = timer(this.config.busyMinDurationMs + this.config.busyDelayMs); this.busyDelayTimer = timer(this.config.busyDelayMs); var busyDelayTimerStart = this.taskStart$.pipe(switchMap(function () { return _this.busyDelayTimer; })); var busyDelayTimerEnd = busyDelayTimerStart.pipe(takeUntil(this.taskEnd$)); var emitOnTaskEnd = this.taskEnd$.pipe(mapTo(1)); var emitOnDelayTimerEnd = busyDelayTimerEnd.pipe(mapTo(-1)); var emitOnMinDurationEnd = this.busyMinDurationTimer.pipe(mapTo(-1)); // Start loading skeleton var raceBetweenTaskAndDelay = combineLatest([ emitOnTaskEnd.pipe(startWith(null)), emitOnDelayTimerEnd.pipe(startWith(null)), ]).pipe(skip(1)); var taskEndBeforeDelay = raceBetweenTaskAndDelay.pipe(filter(function (_a) { var _b = __read(_a, 2), taskEndFirst = _b[0], timerEndFirst = _b[1]; return taskEndFirst === 1 && timerEndFirst === null; })); var shouldNotShowSpinner = taskEndBeforeDelay.pipe(mapTo(false)); var taskEndAfterTimeout = raceBetweenTaskAndDelay.pipe(filter(function (_a) { var _b = __read(_a, 2), taskEndFirst = _b[0], timerEndFirst = _b[1]; return taskEndFirst === null && timerEndFirst === -1; })); var shouldShowSpinner = taskEndAfterTimeout.pipe(mapTo(true)); var showSpinner = shouldShowSpinner.pipe(tap(function () { _this.show(); })); // hide loading skeleton var raceBetweenTaskAndMinDuration = combineLatest([ emitOnTaskEnd.pipe(startWith(null)), emitOnMinDurationEnd.pipe(startWith(null)), ]).pipe(skip(1)); var hideSpinnerUntilMinDurationEnd = raceBetweenTaskAndMinDuration.pipe(filter(function (_a) { var _b = __read(_a, 2), taskEndFirst = _b[0], timerEndFirst = _b[1]; return taskEndFirst === 1 && timerEndFirst === null; })); var hideSpinnerAfterTimerAndTaskEnd = raceBetweenTaskAndMinDuration.pipe(filter(function (_a) { var _b = __read(_a, 2), taskEndFirst = _b[0], timerEndFirst = _b[1]; return taskEndFirst === 1 && timerEndFirst === -1; })); var hideSpinner = merge( // case 1: should not show spinner at all shouldNotShowSpinner, // case 2: task end, but wait until min duration timer ends combineLatest([hideSpinnerUntilMinDurationEnd, emitOnMinDurationEnd]), // case 3: task takes a long time, wait unitl its end hideSpinnerAfterTimerAndTaskEnd).pipe(tap(function () { _this.hide(); })); return showSpinner.pipe(takeUntil(hideSpinner)); }; NgxSuspenseService.prototype.showLoadingStatus = function () { var _this = this; if (this.sub && typeof this.sub.unsubscribe === "function") { this.sub.unsubscribe(); } this.sub = this.controller().subscribe(); return function (source) { return new Observable(function (subscriber) { var emitOnObsEnd = source.pipe(tap(function () { _this.taskEndSubject.next(); })); var sub = of(null) .pipe(tap(function () { _this.taskStartSubject.next(); }), concatMap(function () { return emitOnObsEnd; }), shareReplay(1)) .subscribe(subscriber); return function () { sub.unsubscribe(); }; }); }; }; NgxSuspenseService.prototype.showingFor = function (obs$) { var _this = this; if (this.sub && typeof this.sub.unsubscribe === "function") { this.sub.unsubscribe(); } this.sub = this.controller().subscribe(); var emitOnObsEnd = obs$.pipe(tap(function () { _this.taskEndSubject.next(); })); return of(null).pipe(tap(function () { _this.taskStartSubject.next(); }), concatMap(function () { return emitOnObsEnd; }), shareReplay(1)); }; NgxSuspenseService.ctorParameters = function () { return [ { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [LOADING_CONFIG_TOKEN,] }] } ]; }; NgxSuspenseService = __decorate([ Injectable(), __param(0, Optional()), __param(0, Inject(LOADING_CONFIG_TOKEN)) ], NgxSuspenseService); return NgxSuspenseService; }()); var NgxSuspenseComponent = /** @class */ (function () { function NgxSuspenseComponent(loadingService) { this.loadingService = loadingService; this.ariaLabel = "Loading..."; this.isVisible = false; } NgxSuspenseComponent.prototype.ngOnInit = function () { this.service = this.getService(); this.loading$ = this.service.loading$; }; NgxSuspenseComponent.prototype.getService = function () { return this.bind || this.loadingService; }; NgxSuspenseComponent.prototype.show = function () { this.service.show(); this.isVisible = true; }; NgxSuspenseComponent.prototype.hide = function () { this.service.hide(); this.isVisible = false; }; NgxSuspenseComponent.ctorParameters = function () { return [ { type: NgxSuspenseService } ]; }; __decorate([ Input() ], NgxSuspenseComponent.prototype, "fallback", void 0); __decorate([ Input() ], NgxSuspenseComponent.prototype, "ariaLabel", void 0); __decorate([ Input() ], NgxSuspenseComponent.prototype, "bind", void 0); __decorate([ Input() ], NgxSuspenseComponent.prototype, "isVisible", void 0); NgxSuspenseComponent = __decorate([ Component({ selector: "Suspense", template: "<ng-template #content>\n <!-- animation doesn't work with ng-template, ng-content or ng-container, have to add extra div for workaround-->\n <div [@fadeIn]>\n <ng-content></ng-content>\n </div>\n</ng-template>\n\n<div\n *ngIf=\"(loading$ | async) || isVisible; else content\"\n role=\"alert\"\n aria-busy=\"true\"\n aria-hidden=\"false\"\n [attr.aria-label]=\"ariaLabel\"\n>\n <ng-container *ngTemplateOutlet=\"fallback\"></ng-container>\n</div>\n", animations: [ trigger("fadeIn", [ transition(":enter", [ style({ opacity: 0 }), animate("300ms ease-in", style({ opacity: 1, })), ]), ]), ], styles: ["\n :host {\n display: block;\n }\n "] }) ], NgxSuspenseComponent); return NgxSuspenseComponent; }()); var NgxSuspenseListComponent = /** @class */ (function () { function NgxSuspenseListComponent() { this.revealOrder = "*"; this.subs = []; this.allBroadcasters = []; this.allListeners = []; this.hasParentControlSubject = new BehaviorSubject(undefined); this.parentControl$ = this.hasParentControlSubject.asObservable(); } NgxSuspenseListComponent_1 = NgxSuspenseListComponent; NgxSuspenseListComponent.prototype.ngOnInit = function () { this.parentControl$ .pipe(tap(function (val) { console.log("release", val); })) .subscribe(); }; NgxSuspenseListComponent.prototype.ngAfterContentInit = function () { var _this = this; this.allBroadcasters = this.skeletons.map(function (s) { return s.loading$; }); this.allListeners = this.skeletons.map(function (s) { return _this.hideSkeletonListener(s); }); this.revealOrderOperator(this.revealOrder); }; NgxSuspenseListComponent.prototype.ngOnChanges = function (changes) { if (changes.revealOrder) { this.revealOrderOperator(changes.revealOrder.currentValue); } }; NgxSuspenseListComponent.prototype.ngOnDestroy = function () { if (this.subs.length) { this.subs.forEach(function (sub) { return sub.unsubscribe(); }); this.subs.length = 0; } }; NgxSuspenseListComponent.prototype.reload = function (order) { this.revealOrderOperator(order || this.revealOrder); }; NgxSuspenseListComponent.prototype.show = function () { }; NgxSuspenseListComponent.prototype.hide = function () { }; NgxSuspenseListComponent.prototype.revealOrderOperator = function (order) { if (this.allBroadcasters.length === 0 || this.allListeners.length === 0) { return; } if (this.subs.length) { this.subs.forEach(function (sub) { return sub.unsubscribe(); }); this.subs.length = 0; } switch (order) { case "together": { var sub = this.togetherOperator(this.allBroadcasters)(this.allListeners); this.subs.push(sub); break; } case "forwards": { var subs = this.domOrderOperator(this.allBroadcasters)(this.allListeners); this.subs = subs; break; } case "backwards": { var broadcasters = __spread(this.allBroadcasters).reverse(); var listeners = __spread(this.allListeners).reverse(); var subs = this.domOrderOperator(broadcasters)(listeners); this.subs = subs; break; } default: // nothing should happen } }; NgxSuspenseListComponent.prototype.hideSkeletonListener = function (skeleton) { return function () { skeleton.hide(); }; }; NgxSuspenseListComponent.prototype.showSkeletonListener = function (skeleton) { return function () { skeleton.show(); }; }; NgxSuspenseListComponent.prototype.togetherOperator = function (broadcasters) { var _this = this; this.skeletons.forEach(function (s) { return _this.showSkeletonListener(s)(); }); return function (listeners) { return combineLatest(__spread(broadcasters)) .pipe( // skip default skip(1), // keep tracking previous value pairwise(), filter(function (_a) { var _b = __read(_a, 2), ary1 = _b[0], ary2 = _b[1]; // any loading state is ture var cond1 = ary1.some(function (b) { return b; }); // all loading state is false var cond2 = !ary2.some(function (b) { return b; }); // pass only when prev loading state is ture, current is false return cond1 && cond2; })) .subscribe(function () { listeners.forEach(function (hide) { return hide(); }); }); }; }; NgxSuspenseListComponent.prototype.domOrderOperator = function (broadcasters) { var _this = this; this.skeletons.forEach(function (s) { return _this.showSkeletonListener(s)(); }); return function (listeners) { var subs = []; var checks = __spread(new Array(broadcasters.length)).fill(null); broadcasters.forEach(function (boradcaster, index) { subs.push(boradcaster .pipe( // skip default skip(1), // keep tracking previous value pairwise(), filter(function (_a) { var _b = __read(_a, 2), b1 = _b[0], b2 = _b[1]; // pass only when prev loading state is ture, current is false return b1 && !b2; })) .subscribe(function () { var e_1, _a; checks[index] = true; var indexesToFlush = _this.getFlushIndexes(checks); try { for (var indexesToFlush_1 = __values(indexesToFlush), indexesToFlush_1_1 = indexesToFlush_1.next(); !indexesToFlush_1_1.done; indexesToFlush_1_1 = indexesToFlush_1.next()) { var i = indexesToFlush_1_1.value; listeners[i](); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (indexesToFlush_1_1 && !indexesToFlush_1_1.done && (_a = indexesToFlush_1.return)) _a.call(indexesToFlush_1); } finally { if (e_1) throw e_1.error; } } })); }); return subs; }; }; NgxSuspenseListComponent.prototype.getFlushIndexes = function (checks) { var indexesToFlush = []; for (var i = 0; i < checks.length; i++) { if (indexesToFlush.indexOf(i) > -1) { continue; } var check = checks[i]; if (check !== null) { indexesToFlush.push(i); } else { break; } } return indexesToFlush; }; var NgxSuspenseListComponent_1; __decorate([ Input() ], NgxSuspenseListComponent.prototype, "revealOrder", void 0); __decorate([ ContentChildren(NgxSuspenseComponent) ], NgxSuspenseListComponent.prototype, "skeletons", void 0); __decorate([ ContentChildren(NgxSuspenseListComponent_1) ], NgxSuspenseListComponent.prototype, "list", void 0); NgxSuspenseListComponent = NgxSuspenseListComponent_1 = __decorate([ Component({ selector: "SuspenseList", template: "<ng-content></ng-content>\n", exportAs: "list" }) ], NgxSuspenseListComponent); return NgxSuspenseListComponent; }()); var ɵ0 = LOADING_DEFUALT_CONFIG; var NgxSuspenseModule = /** @class */ (function () { function NgxSuspenseModule() { } NgxSuspenseModule_1 = NgxSuspenseModule; NgxSuspenseModule.forRoot = function (config) { return { ngModule: NgxSuspenseModule_1, providers: [ { provide: LOADING_CONFIG_TOKEN, useValue: __assign(__assign({}, LOADING_DEFUALT_CONFIG), config), }, ], }; }; var NgxSuspenseModule_1; NgxSuspenseModule = NgxSuspenseModule_1 = __decorate([ NgModule({ declarations: [NgxSuspenseListComponent, NgxSuspenseComponent], imports: [BrowserAnimationsModule, CommonModule], providers: [ { provide: LOADING_CONFIG_TOKEN, useValue: ɵ0, }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], exports: [NgxSuspenseComponent, NgxSuspenseListComponent], }) ], NgxSuspenseModule); return NgxSuspenseModule; }()); /* * Public API Surface of ngx-suspense */ /** * Generated bundle index. Do not edit. */ export { LOADING_CONFIG_TOKEN, LOADING_DEFUALT_CONFIG, NgxSuspenseComponent, NgxSuspenseListComponent, NgxSuspenseModule, NgxSuspenseService, ɵ0 }; //# sourceMappingURL=ngx-suspense.js.map