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.

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

Top comments (0)

AWS Q Developer image

Your AI Code Assistant

Implement features, document your code, or refactor your projects.
Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay