Reputation: 158
I'm trying to show a spinner during the application's HTTP requests on Angular 7. I've created a HttpInterceptor class, a loader service and a loader component and a Subject representing the state of the spinner (shown or hidden). However there seems to be a problem between the loader service and the loader component
The HttpInterceptor seems to be working fine as the console outputs true and false states (code below), however there seems to be a problem with the loader service and loader component. I've used an ngIf directive to show or hide the spinner according to the state of a Subject.
LoaderService:
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class LoaderService {
isLoading = new Subject<boolean>();
show() {
console.log('true');
this.isLoading.next(true);
}
hide() {
console.log('false');
this.isLoading.next(false);
}
constructor() { }
}
LoaderComponent
import { Component, OnInit } from '@angular/core';
import {LoaderService} from '../loader.service';
import { Subject } from 'rxjs';
@Component({
selector: 'app-loader',
templateUrl: './loader.component.html',
styleUrls: ['./loader.component.css']
})
export class LoaderComponent implements OnInit {
isLoading: Subject<boolean> = this.loaderService.isLoading;
constructor(private loaderService: LoaderService) { }
ngOnInit() {
}
}
<div *ngIf="isLoading | async " class="d-flex justify-content-center">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
Currently the spinner isn't showing or hiding at all. I'm only getting 'true' in the console when the loading begins, then 'false' when it ends. I've included the loader selector in the app.component.html file. It should be working properly. Did I do something wrong?
Upvotes: 3
Views: 2622
Reputation: 54821
Creating an HttpInterceptor
that can emit a boolean state of HTTP requests is very tricky. There can be multiple parallel requests happening at the sametime, and it's not as simple as emitting true/false for each HttpRequest
. You need to emit true only once when a request starts but emit false when the last request completes.
The easiest way is to keep track of the number of concurrent requests.
Here's the interceptor that I use. It only emits true when a new request is started, and only emits false when all requests have finished.
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {Observable, Subject} from 'rxjs';
import {distinctUntilChanged, finalize, map} from 'rxjs/operators';
class BusySubject extends Subject<number> {
private _previous: number = 0;
public next(value: number) {
this._previous += value;
if (this._previous < 0) {
throw new Error('unexpected negative value');
}
return super.next(this._previous);
}
}
@Injectable()
export class BusyInterceptor implements HttpInterceptor {
private _requests: BusySubject = new BusySubject();
public get busy(): Observable<boolean> {
return this._requests.pipe(
map((requests: number) => Boolean(requests)),
distinctUntilChanged(),
);
}
public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
this._requests.next(1);
return next.handle(req).pipe(finalize(() => this._requests.next(-1)));
}
}
p.s. This could properly be done using a scan() operator, but I haven't had time to update the code.
Upvotes: 2
Reputation: 2213
You should use a BehaviourSubject
instead of a Subject
. Subject
only emits values when next()
is called - if the component is initialised after the service has pushed the value then nothing will ever be received.
BehaviourSubject
gets around this by remembering the last emitted value. When your template subscribes it can therefore get the last value and do its comparison.
Upvotes: 1