diff --git a/src/lib/components/ng-http-loader.component.ts b/src/lib/components/ng-http-loader.component.ts index 64aa1f4..89cd093 100644 --- a/src/lib/components/ng-http-loader.component.ts +++ b/src/lib/components/ng-http-loader.component.ts @@ -8,8 +8,8 @@ */ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { EMPTY, merge, Observable, Subscription, timer } from 'rxjs'; -import { debounce, delayWhen } from 'rxjs/operators'; +import { merge, Observable, Subscription, timer } from 'rxjs'; +import { debounce, distinctUntilChanged, partition, switchMap } from 'rxjs/operators'; import { PendingInterceptorService } from '../services/pending-interceptor.service'; import { SpinnerVisibilityService } from '../services/spinner-visibility.service'; import { Spinkit } from '../spinkits'; @@ -23,7 +23,7 @@ export class NgHttpLoaderComponent implements OnDestroy, OnInit { public isSpinnerVisible: boolean; public spinkit = Spinkit; private subscriptions: Subscription; - private startTime: number; + private visibleUntil: number = Date.now(); @Input() public backgroundColor: string; @@ -40,16 +40,21 @@ export class NgHttpLoaderComponent implements OnDestroy, OnInit { @Input() public minDuration = 0; @Input() + public extraDuration = 0; + @Input() public entryComponent: any = null; constructor(private pendingInterceptorService: PendingInterceptorService, private spinnerVisibilityService: SpinnerVisibilityService) { + const [showSpinner, hideSpinner] = partition((h: boolean) => h)(this.pendingInterceptorService.pendingRequestsStatus$); + this.subscriptions = merge( - this.pendingInterceptorService.pendingRequestsStatus$.pipe( - debounce(h => this.handleDebounceDelay(h)), - delayWhen(h => this.handleMinDuration(h)) + showSpinner.pipe(debounce(() => timer(this.debounceDelay))), + showSpinner.pipe( + switchMap(() => hideSpinner.pipe(debounce(() => this.getHiddingTimer()))) ), this.spinnerVisibilityService.visibilityObservable$, ) + .pipe(distinctUntilChanged()) .subscribe(h => this.handleSpinnerVisibility(h)); } @@ -68,13 +73,13 @@ export class NgHttpLoaderComponent implements OnDestroy, OnInit { } } - private initFilters() { + private initFilters(): void { this.initFilteredUrlPatterns(); this.initFilteredMethods(); this.initFilteredHeaders(); } - private initFilteredUrlPatterns() { + private initFilteredUrlPatterns(): void { if (!(this.filteredUrlPatterns instanceof Array)) { throw new TypeError('`filteredUrlPatterns` must be an array.'); } @@ -86,45 +91,29 @@ export class NgHttpLoaderComponent implements OnDestroy, OnInit { } } - private initFilteredMethods() { + private initFilteredMethods(): void { if (!(this.filteredMethods instanceof Array)) { throw new TypeError('`filteredMethods` must be an array.'); } this.pendingInterceptorService.filteredMethods = this.filteredMethods; } - private initFilteredHeaders() { + private initFilteredHeaders(): void { if (!(this.filteredHeaders instanceof Array)) { throw new TypeError('`filteredHeaders` must be an array.'); } this.pendingInterceptorService.filteredHeaders = this.filteredHeaders; } - private handleSpinnerVisibility(hasPendingRequests: boolean): void { - this.isSpinnerVisible = hasPendingRequests; - } - - private handleDebounceDelay(hasPendingRequests: boolean): Observable { - if (hasPendingRequests && !!this.debounceDelay) { - return timer(this.debounceDelay); - } - - return EMPTY; - } - - private handleMinDuration(hasPendingRequests: boolean): Observable { - if (hasPendingRequests || !this.minDuration) { - if (this.shouldInitStartTime()) { - this.startTime = Date.now(); - } - - return timer(0); + private handleSpinnerVisibility(showSpinner: boolean): void { + const now = Date.now(); + if (showSpinner && this.visibleUntil <= now) { + this.visibleUntil = now + this.minDuration; } - - return timer(this.minDuration - (Date.now() - this.startTime)); + this.isSpinnerVisible = showSpinner; } - private shouldInitStartTime(): boolean { - return !this.isSpinnerVisible; + private getHiddingTimer(): Observable { + return timer(Math.max(this.extraDuration, this.visibleUntil - Date.now())); } } diff --git a/src/test/components/ng-http-loader.component.spec.ts b/src/test/components/ng-http-loader.component.spec.ts index ce72561..671b9e4 100644 --- a/src/test/components/ng-http-loader.component.spec.ts +++ b/src/test/components/ng-http-loader.component.spec.ts @@ -518,15 +518,50 @@ describe('NgHttpLoaderComponent', () => { const secondRequest = httpMock.expectOne('/fake2'); expect(component.isSpinnerVisible).toBeTruthy(); - // After 900ms, the second http request ends. The spinner should + // After 900ms, the spinner should // still be visible because the second HTTP request is still pending tick(900); expect(component.isSpinnerVisible).toBeTruthy(); // 500 ms later, the second http request ends. The spinner should be hidden - // Total time spent visible (1000+200+900+500==2600 > minDuration) + // Total time spent visible (1000+200+1400==2600 > minDuration) tick(500); secondRequest.flush({}); + tick(); + expect(component.isSpinnerVisible).toBeFalsy(); + } + ))); + + it('should handle the extra spinner duration for multiple HTTP requests ran one after the others', fakeAsync(inject( + [HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => { + component.extraDuration = 10; + + function runQuery(url: string): Observable { + return http.get(url); + } + + runQuery('/fake').subscribe(); + const firstRequest = httpMock.expectOne('/fake'); + + tick(1000); + expect(component.isSpinnerVisible).toBeTruthy(); + + // the first HTTP request is finally over, the spinner is still visible for at least 10ms + firstRequest.flush({}); + tick(5); + expect(component.isSpinnerVisible).toBeTruthy(); + + // But 5 ms after the first HTTP request has finished, a second HTTP request has been launched + runQuery('/fake2').subscribe(); + const secondRequest = httpMock.expectOne('/fake2'); + + // After 700ms, the second http request ends. The spinner is still visible + tick(700); + secondRequest.flush({}); + expect(component.isSpinnerVisible).toBeTruthy(); + + // 10ms later, the spinner should be hidden (extraDuration) + tick(10); expect(component.isSpinnerVisible).toBeFalsy(); } )));