Basically, I needed a means to provide a spinner that blocked functionality while API calls were in-flight. Additionally, I wanted to take into account that there could be more than one API request in-flight, at one time.
Repository
A Failed Attempt
My first attempt was to use an interceptor service that contained a BehaviorSubject
(Observable). I set it up to maintain a counter and set the observable's value to true
if there were more than zero (0) requests in-flight.
Through heavy use of the console.log
functionality, I came to realize that the interceptor was not always active, even though I was following proper singleton patterns.
Working Version
The second attempt went more smoothly.
I had a second service (a handler) that maintained the counts and the BehaviorSubject
. This one worked "like a charm."
Spinner Interceptor Service
spinner-interceptor.service.ts
import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; import { Observable } from 'rxjs'; import { finalize } from 'rxjs/operators'; import { SpinnerHandlerService } from './spinner-handler.service'; @Injectable() export class SpinnerInterceptorService implements HttpInterceptor { constructor( public spinnerHandler: SpinnerHandlerService ) {} intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { this.spinnerHandler.handleRequest('plus'); return next .handle(request) .pipe( finalize(this.finalize.bind(this)) ); } finalize = (): void => this.spinnerHandler.handleRequest(); }
Unit Tests ...
spinner-interceptor.service.spec.ts
import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; import { SpinnerInterceptorService } from './spinner-interceptor.service'; import { SpinnerHandlerService } from './spinner-handler.service'; describe('SpinnerInterceptorInterceptor', () => { let service: SpinnerInterceptorService; beforeEach(async () => { TestBed.configureTestingModule({ providers: [ SpinnerInterceptorService, SpinnerHandlerService ] }).compileComponents(); }); beforeEach(() => { service = TestBed.inject(SpinnerInterceptorService); }); it('should be created', () => { expect(service).toBeTruthy(); }); it('expects "intercept" to fire handleRequest', (done: DoneFn) => { const handler: any = { handle: () => { return of(true); } }; const request: any = { urlWithParams: '/api', clone: () => { return {}; } }; spyOn(service.spinnerHandler, 'handleRequest').and.stub(); service.intercept(request, handler).subscribe(response => { expect(response).toBeTruthy(); expect(service.spinnerHandler.handleRequest).toHaveBeenCalled(); done(); }); }); it('expects "finalize" to fire handleRequest', () => { spyOn(service.spinnerHandler, 'handleRequest').and.stub(); service.finalize(); expect(service.spinnerHandler.handleRequest).toHaveBeenCalled(); }); });
Spinner Handler Service
spinner-handler.service.ts
import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class SpinnerHandlerService { public numberOfRequests: number = 0; public showSpinner: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); handleRequest = (state: string = 'minus'): void => { this.numberOfRequests = (state === 'plus') ? this.numberOfRequests + 1 : this.numberOfRequests - 1; this.showSpinner.next(this.numberOfRequests > 0); }; }
Spinner Component
spinner.component.ts
import { Component } from '@angular/core'; import { SpinnerHandlerService } from '@core/services/spinner-handler.service'; @Component({ selector: 'spinner', templateUrl: './spinner.component.html', styleUrls: ['./spinner.component.scss'] }) export class SpinnerComponent { spinnerActive: boolean = true; constructor( public spinnerHandler: SpinnerHandlerService ) { this.spinnerHandler.showSpinner.subscribe(this.showSpinner.bind(this)); } showSpinner = (state: boolean): void => { this.spinnerActive = state; }; }
spinner.component.html
<div class="spinner-container" *ngIf="spinnerActive"> <mat-spinner></mat-spinner> </div>
spinner.component.scss
.spinner-container { background-color: rgba(0,0,0, 0.1); position: fixed; left: 0; top: 0; height: 100vh; width: 100vw; display: flex; align-items: center; justify-content: center; z-index: 10000 }
One More Thing
Don't forget to add the interceptor service into app.module.ts
...
providers: [ { provide: HTTP_INTERCEPTORS, useClass: SpinnerInterceptorService, multi: true } ],
Repository
Conclusion
This pattern is a reasonable one and the observable can be used in a variety of scenarios.
Top comments (0)