ngx-suspense
Version:
This library is an experimetnal implementation of React Suspense for Angular.
431 lines (422 loc) • 18.1 kB
JavaScript
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