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;
}