Angular: Spinner Interceptor
Tags: angular, development, programming, spinner
Published: 2021-10-25 16:41:25

---
title: Angular: Spinner Interceptor
published: true
description: Basically, I needed a means to provide a spinner that blocked functionality while API calls were in-flight. Additionally, I wanted to take into account that there could be more than one API request in-flight, at one time.
series: Angular
tags: Angular, Development, Programming, Spinner
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sb65ez55ncaza41h4t8k.png
---

Basically, I needed a means to provide a spinner that blocked functionality while API calls were in-flight. Additionally, I wanted to take into account that there could be more than one API request in-flight, at one time.

## Repository

* [DEMO HERE](https://bob-fornal.github.io/angular-spinner-interceptor/)
* [REPO HERE](https://github.com/bob-fornal/angular-spinner-interceptor)

## A Failed Attempt

My first attempt was to use an interceptor service that contained a `BehaviorSubject` (Observable). I set it up to maintain a counter and set the observable's value to `true` if there were more than zero (0) requests in-flight.

Through heavy use of the `console.log` functionality, I came to realize that the interceptor was not always active, even though I was following proper singleton patterns.

## Working Version

The second attempt went more smoothly.

I had a second service (a handler) that maintained the counts and the `BehaviorSubject`. This one worked "like a charm."

## Spinner Interceptor Service

`spinner-interceptor.service.ts`

```typescript
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';

import { SpinnerHandlerService } from './spinner-handler.service';

@Injectable()
export class SpinnerInterceptorService implements HttpInterceptor {

  constructor(
    public spinnerHandler: SpinnerHandlerService
  ) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    this.spinnerHandler.handleRequest('plus');
    return next
      .handle(request)
      .pipe(
        finalize(this.finalize.bind(this))
      );
  }

  finalize = (): void => this.spinnerHandler.handleRequest();

}
```

Unit Tests ...

`spinner-interceptor.service.spec.ts`

```typescript
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';

import { SpinnerInterceptorService } from './spinner-interceptor.service';

import { SpinnerHandlerService } from './spinner-handler.service';

describe('SpinnerInterceptorInterceptor', () => {
  let service: SpinnerInterceptorService;

  beforeEach(async () => {
    TestBed.configureTestingModule({
      providers: [
        SpinnerInterceptorService,
        SpinnerHandlerService
      ]
    }).compileComponents();
  });

  beforeEach(() => {
    service = TestBed.inject(SpinnerInterceptorService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('expects "intercept" to fire handleRequest', (done: DoneFn) => {
    const handler: any = {
      handle: () => {
        return of(true);
      }
    };
    const request: any = {
      urlWithParams: '/api',
      clone: () => {
        return {};
      }
    };
    spyOn(service.spinnerHandler, 'handleRequest').and.stub();

    service.intercept(request, handler).subscribe(response => {
      expect(response).toBeTruthy();
      expect(service.spinnerHandler.handleRequest).toHaveBeenCalled();
      done();
    });
  });

  it('expects "finalize" to fire handleRequest', () => {
    spyOn(service.spinnerHandler, 'handleRequest').and.stub();

    service.finalize();
    expect(service.spinnerHandler.handleRequest).toHaveBeenCalled();
  });

});
```

## Spinner Handler Service

`spinner-handler.service.ts`

```typescript
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class SpinnerHandlerService {

  public numberOfRequests: number = 0;
  public showSpinner: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);

  handleRequest = (state: string = 'minus'): void => {
    this.numberOfRequests = (state === 'plus') ? this.numberOfRequests + 1 : this.numberOfRequests - 1;
    this.showSpinner.next(this.numberOfRequests > 0);
  };

}
```

## Spinner Component

`spinner.component.ts`

```typescript
import { Component } from '@angular/core';

import { SpinnerHandlerService } from '@core/services/spinner-handler.service';

@Component({
  selector: 'spinner',
  templateUrl: './spinner.component.html',
  styleUrls: ['./spinner.component.scss']
})
export class SpinnerComponent {

  spinnerActive: boolean = true;

  constructor(
    public spinnerHandler: SpinnerHandlerService
  ) {
    this.spinnerHandler.showSpinner.subscribe(this.showSpinner.bind(this));
  }

  showSpinner = (state: boolean): void => {
    this.spinnerActive = state;
  };

}
```

`spinner.component.html`

```html
<div class="spinner-container" *ngIf="spinnerActive">
  <mat-spinner></mat-spinner>
</div>
```

`spinner.component.scss`

```scss
.spinner-container {
  background-color: rgba(0,0,0, 0.1);
  position: fixed;
  left: 0;
  top: 0;
  height: 100vh;
  width: 100vw;

  display: flex;
  align-items: center;
  justify-content: center;

  z-index: 10000
}
```

## One More Thing

Don't forget to add the interceptor service into `app.module.ts` ...

```typescript
providers: [
  { provide: HTTP_INTERCEPTORS, useClass: SpinnerInterceptorService, multi: true }
],
```
## Repository

* [DEMO HERE](https://bob-fornal.github.io/angular-spinner-interceptor/)
* [REPO HERE](https://github.com/bob-fornal/angular-spinner-interceptor)

## Conclusion

This pattern is a reasonable one and the observable can be used in a variety of scenarios.