Angular2: Show activity indicator for every HTTP request and hide views until completed
Solution 1
One way is to write an interceptor for Angular2 Http. By creating your own http instance you can swap that in when you are bootstrapping your application by use of the "provide" method. Once this is done a PubSub service can be created to publish and subscribe to these events from your Http interceptor and emit before and after events on every request made.
A live example can be seen on Plunker
The Interceptor:
import {Injectable} from 'angular2/core';
import {HTTP_PROVIDERS, Http, Request, RequestOptionsArgs, Response, XHRBackend, RequestOptions, ConnectionBackend, Headers} from 'angular2/http';
import 'rxjs/Rx';
import {PubSubService} from './pubsubService';
@Injectable()
export class CustomHttp extends Http {
_pubsub: PubSubService
constructor(backend: ConnectionBackend, defaultOptions: RequestOptions, pubsub: PubSubService) {
super(backend, defaultOptions);
this._pubsub = pubsub;
}
request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
return this.intercept(super.request(url, options));
}
get(url: string, options?: RequestOptionsArgs): Observable<Response> {
return this.intercept(super.get(url,options));
}
post(url: string, body: string, options?: RequestOptionsArgs): Observable<Response> {
return this.intercept(super.post(url, body, this.getRequestOptionArgs(options)));
}
put(url: string, body: string, options?: RequestOptionsArgs): Observable<Response> {
return this.intercept(super.put(url, body, this.getRequestOptionArgs(options)));
}
delete(url: string, options?: RequestOptionsArgs): Observable<Response> {
return this.intercept(super.delete(url, options));
}
getRequestOptionArgs(options?: RequestOptionsArgs) : RequestOptionsArgs {
if (options == null) {
options = new RequestOptions();
}
if (options.headers == null) {
options.headers = new Headers();
}
options.headers.append('Content-Type', 'application/json');
return options;
}
intercept(observable: Observable<Response>): Observable<Response> {
this._pubsub.beforeRequest.emit("beforeRequestEvent");
//this will force the call to be made immediately..
observable.subscribe(
null,
null,
() => this._pubsub.afterRequest.emit("afterRequestEvent");
);
return observable
}
}
The Emitters
import {Subject } from 'rxjs/Subject';
export class RequestEventEmitter extends Subject<String>{
constructor() {
super();
}
emit(value) { super.next(value); }
}
export class ResponseEventEmitter extends Subject<String>{
constructor() {
super();
}
emit(value) { super.next(value); }
}
The PubSubService
import {Injectable} from 'angular2/core';
import {RequestEventEmitter, ResponseEventEmitter} from './emitter';
@Injectable()
export class PubSubService{
beforeRequest:RequestEventEmitter;
afterRequest:ResponseEventEmitter;
constructor(){
this.beforeRequest = new RequestEventEmitter();
this.afterRequest = new ResponseEventEmitter();
}
}
Bootstrapping the App
//main entry point
import {bootstrap} from 'angular2/platform/browser';
import {provide} from 'angular2/core';
import {Http, HTTP_PROVIDERS, XHRBackend, RequestOptions} from 'angular2/http';
import {HelloWorldComponent} from './hello_world';
import {CustomHttp} from './customhttp';
import {PubSubService} from './pubsubService'
bootstrap(HelloWorldComponent, [HTTP_PROVIDERS,PubSubService,
provide(Http, {
useFactory: (backend: XHRBackend, defaultOptions: RequestOptions, pubsub: PubSubService)
=> new CustomHttp(backend, defaultOptions, pubsub),
deps: [XHRBackend, RequestOptions, PubSubService]
})
]).catch(err => console.error(err));
Now in your loading component its as easy as subscribing to the events and setting a property to show or not
export class LoaderComponent implements OnInit {
showLoader = false;
_pubsub:PubSubService;
constructor(pubsub: PubSubService) {
this._pubsub = pubsub;
}
ngOnInit() {
this._pubsub.beforeRequest.subscribe(data => this.showLoader = true);
this._pubsub.afterRequest.subscribe(data => this.showLoader = false);
}
}
While this ends up being a bit more code, if you are looking to be notified on every request in your application this would do it. One thing to note with the interceptor is since a subscribe is being done for every request immediately all requests will be executed, which may not be what you need in particular cases. A solution to that is to support the regular Angular2 Http and use the CustomHttp as a second option that could be injected where needed. I would think in most cases immediate subscription would work fine. I would love to hear examples of when it would not.
Solution 2
Yes, you need to handle that for each View :
- You can have a service for the http request which will return an Observable
- In the Component you will have a loading state
- You need to set the loading state to true before you request the data from the server, then set it to false when the data fetch is done.
- In the template use the ngIf to hide/show loading or the content
Ex :
The Service :
@Injectable()
export class DataService {
constructor(private http: Http) { }
getData() {
return this.http.get('http://jsonplaceholder.typicode.com/posts/2');
}
}
The Component :
@Component({
selector: 'my-app',
template : `
<div *ngIf="loading == true" class="loader">Loading..</div>
<div *ngIf="loading == false">Content.... a lot of content <br> more content</div>`
})
export class App {
loading: boolean;
constructor(private dataService: DataService) { }
ngOnInit() {
// Start loading Data from the Server
this.loading = true;
this.dataService.getData().delay(1500).subscribe(
requestData => {
// Data loading is Done
this.loading = false;
console.log('AppComponent', requestData);
}
}
}
A working example can be found here : http://plnkr.co/edit/HDEDDLOeiHEDd7VQaev5?p=preview
Solution 3
In addition @tibbus response
This is better to set "isLoading" type of Number and keep it in service.
with boolean:
request 1 starts -> spinner on -> request 2 starts -> request 1 ends -> spinner off -> request 2 ends
with number:
request 1 starts -> spinner on -> request 2 starts -> request 1 ends -> request 2 ends -> spinner off
Service
@Injectable()
export class DataService {
constructor(private http: Http) { }
private set fetchCounter(v:number) {
this._fetchCounter = v;
this.isLoadingSource.next(this._fetchCounter > 0)
}
private get fetchCounter() { return this._fetchCounter };
private _fetchCounter:number = 0;
private isLoadingSource = new Subject<boolean>();
public isLoading = this.isLoadingSource.asObservable();
public getData() {
this.fetchCounter++;
return this.http.get('http://jsonplaceholder.typicode.com/posts/2')
.map(r => {
this.fetchCounter--;
return r;
});
}
}
You just need to subscribe to isLoading from any of your components.
Solution 4
Add a common DAL (data access layer) class like this and use this DAL class in your components.
Add loading indicator as a service or component and use your custom styles for it.
export class DAL {
private baseUrl: string = environment.apiBaseUrl;
private getConsolidatedPath(path: string) {
if (path.charAt(0) === '/') {
path = path.substr(1);
}
return `${this.baseUrl}/${path}`;
}
private callStack = [];
private startCall() {
this.loadingIndicator.display(true);
this.callStack.push(1);
}
private endCall() {
this.callStack.pop();
if (this.callStack.length === 0) {
this.loadingIndicator.display(false);
}
}
public get(path: string) {
this.startCall();
return this.http.get(this.getConsolidatedPath(path), { headers: this.getHeaders() })
.map(response => response.json())
.catch(e => this.handleError(e))
.finally(() => this.endCall());
}
public put(path: string, data: any) {
this.startCall();
return this.http.put(this.getConsolidatedPath(path), data, { headers: this.getHeaders() })
.map(response => response.json())
.catch(e => this.handleError(e))
.finally(() => this.endCall());
}
public post(path: string, data: any) {
this.startCall();
return this.http.post(this.getConsolidatedPath(path), data, { headers: this.getHeaders() })
.map(response => response.json())
.catch(e => this.handleError(e))
.finally(() => this.endCall());
}
public delete(path: string, data: any) {
this.startCall();
return this.http.delete(this.getConsolidatedPath(path), { body: data, headers: this.getHeaders() })
.map(response => response.json())
.catch(e => this.handleError(e))
.finally(() => this.endCall());
}
constructor(public http: Http, public loadingIndicator: LoadingIndicatorService) {
}
}
Related videos on Youtube
Carlos Porras
Updated on June 24, 2022Comments
-
Carlos Porras almost 2 years
Im new to Angular2 and I was wondering if there is any way to show an activity indicator for every HTTP request and hide views until completed?
-
Brian Chance almost 8 yearsI think you can use .do instead of subscribing in the intercept - Plunker.
intercept(observable: Observable<Response>): Observable<Response> { this._pubsub.beforeRequest.emit("beforeRequestEvent"); return observable.do(() => this._pubsub.afterRequest.emit("afterRequestEvent")); }
-
Yodacheese over 7 yearsUsing
do()
should be the correct answer as @BrianChance suggested the requests are being made twice when you subscribe to them since they are cold observables. Look at the network tab when running the plunk. -
angryip over 7 yearseasy and simple. great. ty
-
phl over 7 yearsThe implementation described by @d1820, with the
do()
suggested by @BrianChance worked like a charm when used in Angular v2.0.1, but has a slight problem since upgrading to v2.2.0. Ceteris paribus, the callbacks are now called twice (both onSuccess and onError). I searched a bit but couldn't find what's wrong ; I also couldn't find a 2.2.0 CDN to make a plunkr, but it should be easy to reproduce on a local project. Does anyone have any idea what could be amiss ? -
d1820 over 7 yearsAgreed, .do() is the way to go, Plunker has been updated. Thanks for the suggestion
-
phl about 7 yearsFollowing through on my comment from 2 months ago, here's a Plunkr w/ Angular 2.4.1 showing the problem I mentioned then : [plnkr.co/edit/0ervlO5ucBU1JGcKt3S4?p=info]. Fire up the console before running the application. You will see that whenever the HTTP request is executed, we go through the interceptor twice (see the Before request and After request messages in the console). The problem might lie in how I adapted the code to fit in the
@NgModule
paradigm, but I fail to see what I did wrong. Any help would be greatly appreciated ! @BrianChance @Yodacheese -
Brian Chance about 7 yearsWhen I comment out the CustomHttp.request method, it only runs once. Guessing super.get now calls CustomHttp.request which calls the super. Means you might just have to implement request instead of each verb.
-
d1820 about 7 yearsThe issue is the Http lib has changed, and under the covers all the prototype methods (get, post, put, delete) all make a request now to the base Request method with the appropriate options overridden. To fix the issue simply comment out or remove the Post, Put, Delete, Get methods from the CustomHttp module. [Plunker] (embed.plnkr.co/A3rL1l). The intercept only needs to be done at the base Request prototype method.
-
phl about 7 yearsThank you very much, both of you ! Glad to be able to keep using this neat piece of code to properly manage both a spinner and error toasts ! :)