Angular: display a spinner on any component that does an HTTP request

Stefanos Kouroupis - Sep 4 '19 - - Dev Community

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

Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

our extremely simple HttpProgressState denoting
whether a request has started or ended

export enum HttpProgressState {
    start,
    end
}
Enter fullscreen mode Exit fullscreen mode

The single BehaviorSubject property service

@Injectable({
  providedIn: 'root'
})
export class HttpStateService {
  public state = new BehaviorSubject<IHttpState>({} as IHttpState);

  constructor() { }
}
Enter fullscreen mode Exit fullscreen mode

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
      });
    }));
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
    }
....
]
Enter fullscreen mode Exit fullscreen mode

missing interface (see comments)

export interface IHttpState {
    url: string;
    state: HttpProgressState;
}
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . .