DEV Community

Cover image for Angular: Spinner Interceptor
bob.ts
bob.ts

Posted on

3

Angular: Spinner Interceptor

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

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

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

}
Enter fullscreen mode Exit fullscreen mode

Unit Tests ...

spinner-interceptor.service.spec.ts

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

});
Enter fullscreen mode Exit fullscreen mode

Spinner Handler Service

spinner-handler.service.ts

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

}
Enter fullscreen mode Exit fullscreen mode

Spinner Component

spinner.component.ts

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

}
Enter fullscreen mode Exit fullscreen mode

spinner.component.html

<div class="spinner-container" *ngIf="spinnerActive">
  <mat-spinner></mat-spinner>
</div>
Enter fullscreen mode Exit fullscreen mode

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

One More Thing

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

providers: [
  { provide: HTTP_INTERCEPTORS, useClass: SpinnerInterceptorService, multi: true }
],
Enter fullscreen mode Exit fullscreen mode

Repository

Conclusion

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

Image of AssemblyAI

Automatic Speech Recognition with AssemblyAI

Experience near-human accuracy, low-latency performance, and advanced Speech AI capabilities with AssemblyAI's Speech-to-Text API. Sign up today and get $50 in API credit. No credit card required.

Try the API

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Community—every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple “thank you” goes a long way—express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay