Angular show spinner for every HTTP request with very less code changes
Asked Answered
H

9

32

I am working on an existing Angular application. The version is Angular 4.

The application makes HTTP calls to a REST API from lot of various components.

I want to show a custom spinner for every HTTP request. Since this is an existing application, there are lot of places where calls to REST API are made. And changing code one at every places is not a feasible option.

I would like to implement an abstract solution which would solve this problem.

Please suggest if any options.

Huai answered 20/3, 2018 at 13:20 Comment(2)
This question inspired me to write a blogpost about the topic. Most of it is in my answer below, but a prettier version can be read at grensesnittet.computas.com/loading-status-in-angular-done-rightTouter
Link broke, here is the working on: medium.com/compendium/…Kell
R
77

@jornare has a good idea in his solution. He's handling the case for multiple requests. However, the code could be written simpler, without creating new observable and storing requests in memory. The below code also uses RxJS 6 with pipeable operators:

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpInterceptor,
  HttpResponse
} from '@angular/common/http';
import { finalize } from 'rxjs/operators';
import { LoadingService } from '@app/services/loading.service';
import { of } from 'rxjs';

@Injectable()
export class LoadingInterceptor implements HttpInterceptor {
  private totalRequests = 0;

  constructor(private loadingService: LoadingService) { }

  intercept(request: HttpRequest<any>, next: HttpHandler) {
    this.totalRequests++;
    this.loadingService.setLoading(true);

    return next.handle(request).pipe(
      finalize(() => {
        this.totalRequests--;
        if (this.totalRequests === 0) {
          this.loadingService.setLoading(false);
        }
      })
    );
  }
}

Add this interceptor service into your module providers:

@NgModule({
  // ...
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: LoadingInterceptor, multi: true }
  ]
})
export class AppModule { }

Here's an example of the LoadingService implementation:

@Injectable()
export class LoadingService {
  private isLoading$$ = new BehaviorSubject<boolean>(false);
  isLoading$ = this.isLoading$$.asObservable();
  
  setLoading(isLoading: boolean) {
    this.isLoading$$.next(isLoading);
  }
}

And here's how you'd use the LoadingService in a component:

@Component({
  selector: 'app-root',
  template: `
    <ng-container *ngIf="loadingService.isLoading$ | async">
      <i class="loading"></i>
    </ng-container>
    <router-outlet></router-outlet>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
  constructor(public loadingService: LoadingService) {}
}
Risinger answered 12/8, 2018 at 22:2 Comment(7)
I am using this approach and came across a strange issue that when I send a request and receive a response my counter is always decreased by 1 instead of back to zero. I spent some time and found out I missed the line "if (res instanceof HttpResponse) {" when tapping into the response. I see when next.handle fires it returns a HttpEvent of type Sent first, so my counter was actually decreased by catching that. Just want to log it here in case someone has similar issuesTurkoman
when i log in , i do not want this loader. what should i do ?. I want only when i log in.Conservative
@kumaresan-perumal this solution is suitable for a global loader. For local component interactions, you need to have a separate solutionRisinger
@KumaresanPerumal you can create an array of strings containing services to skip for loader, and increment/decrement total req count for services not included in this array.Celerity
I've caught "ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after" error also. The way with function setTimeout() helped me from this comment (github.com/angular/angular/issues/17572#issuecomment-309229619). I've done "setTimeout(() => this.loadingService.setStatus(true));" and "setTimeout(() => this.loadingService.setStatus(false));" respectively.Clifford
@savostyanov-konstantin I added examples of the LoadingService and its usage in a component. With this approach, you should not get errors.Risinger
@DmitryEfimenko I was wrong. finalize() works fine but the order of interceptors should be Http401Interceptor and then HttpLoadingInterceptor. If not, there's a risk that some subscriptions are cancelled but never popped of the stack in the service that handles loading requests.Crissum
T
17

Angular 4+ has a new HttpClient which supports HttpInterceptors. This allows you to insert code that will be run whenever you make a HTTP request.

It is important to notice that HttpRequest are not long-lived Observables, but they terminate after the response. Furthermore, if the observable is unsubscribed before the response has returned, the request is cancelled and neither of the handlers are being processed. You may therefore end up with a "hanging" loader bar, which never goes away. This typically happens if you navigate a bit fast in your application.

To get around this last issue, we need to create a new Observable to be able to attach teardown-logic.

We return this rather than the original Observable. We also need to keep track of all requests made, because we may run more than one request at a time.

We also need a service which can hold and share the state of whether we have pending requests.

@Injectable()
export class MyLoaderService {
    // A BehaviourSubject is an Observable with a default value
    public isLoading = new BehaviorSubject<boolean>(false);

    constructor() {}
}

The Interceptor uses the MyLoaderService

@Injectable()
export class MyLoaderInterceptor implements HttpInterceptor {
    private requests: HttpRequest<any>[] = [];

    constructor(private loaderService: MyLoaderService) { }

    removeRequest(req: HttpRequest<any>) {
        const i = this.requests.indexOf(req);
        this.requests.splice(i, 1);
        this.loaderService.isLoading.next(this.requests.length > 0);
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        this.requests.push(req);
        this.loaderService.isLoading.next(true);
        return Observable.create(observer => {
          const subscription = next.handle(req)
            .subscribe(
            event => {
              if (event instanceof HttpResponse) {
                this.removeRequest(req);
                observer.next(event);
              }
            },
            err => { this.removeRequest(req); observer.error(err); },
            () => { this.removeRequest(req); observer.complete(); });
          // teardown logic in case of cancelled requests
          return () => {
            this.removeRequest(req);
            subscription.unsubscribe();
          };
        });
    }
}

Finally, in our Component, we can use the same MyLoaderService and with the async operator we do not even need to subscribe. Since the source value we want to use is from a service, it should be shared as an Observable so that it gets a rendering scope/zone where it is used. If it is just a value, it may not update your GUI as wanted.

@Component({...})
export class MyComponent {
    constructor(public myLoaderService: MyLoaderService) {}
}

And an example template using async

<div class="myLoadBar" *ngIf="myLoaderService.isLoading | async">Loading!</div>

I assume you know how to provide services and set up modules properly. You can also see a working example at Stackblitz

Touter answered 21/3, 2018 at 11:47 Comment(5)
when i log in , i do not want this loader. what should i do ?. I want only when i log in.Conservative
Hello @Touter , I found this useful. This doesn't work still for some weird scenarios. like the Ctrl + S described here github.com/angular/angular/issues/22324#issuecomment-390972535Peripeteia
Wow, thank you :) That was absolutely also very helpful.Touter
How can I filter the requests in which I want to show the spinner?Unriddle
I guess you would have to add a property to the request when creating it, and filter on it. Maybe add it as a custom header?Touter
R
11

In Angular 5 comes the HttpClient module. You can find more information there.

With this module, come something called interceptors.

They allow you to do something for every HTTP request.

If you migrate from Http to HttpClient (and you should, Http will be deprecated), you can create an interceptor that can handle a variable in a shared service :

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    this.sharedService.loading = true;
    return next
      .handle(req)
      .finally(() => this.sharedService.loading = false);
}

Now you just have to use this variable into your templates.

<spinner *ngIf="sharedService.loading"></spinner>

(Be sure to have an injection of your service in the components that display this spinner)

Ric answered 20/3, 2018 at 13:25 Comment(0)
G
2

This is a basic loading dialog that can be toggled with an angular property. Just add *ngIf="loader" to the center-loader and set the property appropriately

.center-loader {
    font-size: large;
    position:absolute;
    z-index:1000;
    top: 50%;
    left: 50%;
    -ms-transform: translate(-50%, -50%);
    transform: translate(-50%, -50%);
}

@keyframes blink {50% { color: transparent }}
.loader__dot { animation: 1s blink infinite; font-size: x-large;}
.loader__dot:nth-child(2) { animation-delay: 250ms; font-size: x-large;}
.loader__dot:nth-child(3) { animation-delay: 500ms; font-size: x-large;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.2.3/jquery.min.js"></script>
<div class="center-loader">
  <strong>Loading
  <span class="loader__dot">.</span>
  <span class="loader__dot">.</span>
  <span class="loader__dot">.</span></strong>
</div>

Initialize the loader to true for each page, and then set to false once the service finished:

Top of component:

export class MyComponent implements OnInit {
    loader:boolean = true;
//...

onInit():

 await this.myService
    .yourServiceCall()
    .then(result => {
        this.resultsSet=result);
        this.loader = false;      // <- hide the loader
      }
    .catch(error => console.log(error));
Gollin answered 20/3, 2018 at 13:26 Comment(0)
B
1

Depends upon the approach you follow to use REST SERVICES

My approach is

  • Create a component and place it somewhere in the application level.
  • Create a service which has counter with increment and decrements methods.

  • This service should decide to show the loader(component) or not by following the below steps.

    Increase the counter each for one request from the client.

    Decrease the counter on each response success and failure

Bulk answered 26/7, 2019 at 9:25 Comment(0)
A
1

Angular Interceptors can be used in a number of ways as they work pretty well in manipulating and managing HTTP calls to communicate that we make from a client-side web application. We can create an utility for showing Mouse Loader using Interceptor.

Please go through the below post for the implementation of LoaderInterceptor:-

Show Loader/Spinner On HTTP Request In Angular using Interceptor

Assiduous answered 11/5, 2020 at 11:18 Comment(0)
P
0

you can use some css/gif to show a spinner, and use it on your interceptor class, or you can simply use tur false to show the gif.

<root-app>
    <div class="app-loading show">
        <div class="spinner"></div>
    </div>
</root-app>
Prevent answered 6/4, 2019 at 13:47 Comment(0)
E
0

I was searching for something that can be used by every component. I put in a counter, so the spinner stops when every request has finished.

So this works quite well:

export class LoadingStatus{
  public counter: number = 0;
  public isLoading = new Subject<boolean>();

  public reset(){
    this.counter = 0;
    this.isLoading.next(false);
  }
}

export function triggerLoading<T>(status: LoadingStatus): (source: Observable<T>) => Observable<T> {
  return (source: Observable<T>): Observable<T> => source.pipe(
    prepare(() => {
      if(status != null){
        status.counter++;
        status.isLoading.next(true)
      }
    }    ),
    finalize(() => {
      if(status != null){
        status.counter--;
        // if there is something like a flikering, maybe use a delay.
        if(status.counter <= 0) {
          status.counter = 0;
          status.isLoading.next(false)
        }
      }
    })
  )
}

And then call it like:

public loadingStatus$ = new LoadingStatus();

public makeRequest(){
   this.myService.load()
    .pipe(triggerLoading(this.loadingStatus$))
    .subscribe(v => {});
}

HTML:

<div class="loading-spinner" *ngIf="loadingStatus$?.isLoading | async"></div>
Embalm answered 10/9, 2020 at 6:37 Comment(0)
C
0

This guy make an awesome job here: angular loader using rxjs, handle concurrency

Basically, you make an interceptor. In the interceptor subscribe to NEVER observable each time a request begins, the first time begin to show the overlay/loading; on each subscription ends unsubscribe, when observable reach 0 subscriptions overlay is removed

Coheir answered 4/11, 2021 at 6:11 Comment(1)
Personal opinion: using the interceptor it's not a good idea, use an operator (see this SO)Jeanicejeanie

© 2022 - 2024 — McMap. All rights reserved.