Angular2: Show activity indicator for every HTTP request and hide views until completed

17,602

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) {
    }

}
Share:
17,602

Related videos on Youtube

Carlos Porras
Author by

Carlos Porras

Updated on June 24, 2022

Comments

  • Carlos Porras
    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
    Brian Chance almost 8 years
    I 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
    Yodacheese over 7 years
    Using 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
    angryip over 7 years
    easy and simple. great. ty
  • phl
    phl over 7 years
    The 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
    d1820 over 7 years
    Agreed, .do() is the way to go, Plunker has been updated. Thanks for the suggestion
  • phl
    phl about 7 years
    Following 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
    Brian Chance about 7 years
    When 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
    d1820 about 7 years
    The 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
    phl about 7 years
    Thank 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 ! :)

Related