This is by definition one of the weirdest, useful and a bit ugly component I've written.
Our goal is to apply
a spinner on top of any component that relies on an HTTP request
First we need to create a simple component that has it can take the size of its parent and has a spinner in the middle. I am using the Angular Material library to make things simpler.
This component is using a single service called HttpStateService
. As we will see in a moment HttpStateService
has only a single property of type BehaviorSubject
. So its basically being used to pass messages back and forth.
So our component subscribes to any messages coming from that subject.
The spinner component also has an @Input()
property which is on which url it should react.
@Component({
selector: 'http-spinner',
templateUrl: './spinner.component.html',
styleUrls: ['./spinner.component.scss']
})
export class SpinnerComponent implements OnInit {
public loading = false;
@Input() public filterBy: string | null = null;
constructor(private httpStateService: HttpStateService) { }
/**
* receives all HTTP requests and filters them by the filterBy
* values provided
*/
ngOnInit() {
this.httpStateService.state.subscribe((progress: IHttpState) => {
if (progress && progress.url) {
if (!this.filterBy) {
this.loading = (progress.state === HttpProgressState.start) ? true : false;
} else if (progress.url.indexOf(this.filterBy) !== -1) {
this.loading = (progress.state === HttpProgressState.start) ? true : false;
}
}
});
}
}
the css
.loading {
position: absolute;
top: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.15);
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
and the html, in which we just either a) display the whole thing or b) not
<div *ngIf="loading" class="loading">
<mat-spinner></mat-spinner>
</div>
our extremely simple HttpProgressState denoting
whether a request has started or ended
export enum HttpProgressState {
start,
end
}
The single BehaviorSubject
property service
@Injectable({
providedIn: 'root'
})
export class HttpStateService {
public state = new BehaviorSubject<IHttpState>({} as IHttpState);
constructor() { }
}
And now the most important bit, the HttpInterceptor
. An HttpInterceptor
is basically a man in the middle
service that intercepts all requests that you might try to do through the HttpClientModule
and manipulate them or react to them before they get fired. Here I have a relatively simple implementation of an HttpInterceptor
. I've added take and delay to underline some powerful capabilities an HttpInterceptor
might have.
Apart from take and delay, I've added one more and that is finalize.
So basically every time the InterceptorService
intercepts a request it sends a message to the HttpStateService
containing the url and a start state.
then on finalize (after the request has finished) sends an end state to the HttpStateService
@Injectable({
providedIn: 'root'
})
export class InterceptorService implements HttpInterceptor {
private exceptions: string[] = [
'login'
];
constructor(
private httpStateService: HttpStateService) {
}
/**
* Intercepts all requests
* - in case of an error (network errors included) it repeats a request 3 times
* - all other error can be handled an error specific case
* and redirects into specific error pages if necessary
*
* There is an exception list for specific URL patterns that we don't want the application to act
* automatically
*
* The interceptor also reports back to the httpStateService when a certain requests started and ended
*/
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (!this.exceptions.every((term: string) => request.url.indexOf(term) === -1)) {
return next.handle(request).pipe(tap((response: any) => {},
(error) => {}));
}
this.httpStateService.state.next({
url: request.url,
state: HttpProgressState.start
});
return next.handle(request).pipe(retryWhen(
error => {
return error.pipe(take(3), delay(1500),
tap((response: any) => {
// ...logic based on response type
// i.e redirect on 403
// or feed the error on a toaster etc
})
);
}
), finalize(() => {
this.httpStateService.state.next({
url: request.url,
state: HttpProgressState.end
});
}));
}
}
Its usage is simple add it to any component that needs a spinner and define which endpoint it needs to listen to.
<http-spinner filterBy="data/products"></http-spinner>
Lastly to add an interceptor on a Module
you just need to add another providers like the following example
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: InterceptorService,
multi: true
}
....
]
missing interface (see comments)
export interface IHttpState {
url: string;
state: HttpProgressState;
}
Top comments (7)
Hi Stefanos,
I really liked your solution, I have implemented similar code in my application, and in both your code and mine the same issue occurs. When I make an isolated call to the backend everything happens normal, but if two calls occur at the same time (at the initialization of a component for example) in the interceptor arrive both, but in the subscribe of the BehaviorSubject only one arrives. I'm really stuck at this, any thoughts why this happens?
I ve only used that component in tables and charts and ...even when I had multiple charts per page, that still works fine. Bare in mind that each char subscribed to a different endpoint. So I've never tested it with multiple requests per component.
Because I find the issue intriguing...I am going to spend sometime tomorrow investigating it.
I found the problem in my application, it was not a problem because there were multiple subscriptions, the problem is that I was making the backend call at the same time as the component creation happened (changing the route and opening a screen did both) and when I made that call my component had not subscribed to BehaviorSubject yet. I just moved my call to the backend to ngAfterViewInit and everything worked as it should.
This component have repository?
No it doesn't. It is too simple, in my opinion, to be turned into a lib.
Hi Stefanos,
you forgot to declare IHttpState.
true, Ill add it in, I wonder how no one noticed it so far.