Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

http

angular

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?

like image 864
Carlos Porras Avatar asked May 27 '16 18:05

Carlos Porras


3 Answers

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.

like image 155
d1820 Avatar answered Nov 16 '22 05:11

d1820


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

like image 30
tibbus Avatar answered Nov 16 '22 05:11

tibbus


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.

like image 3
stjimmy54 Avatar answered Nov 16 '22 04:11

stjimmy54