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.