DEV Community

Tony Shaji
Tony Shaji

Posted on

Disabling button on api calls in Angular

While working on multiple frameworks, I have come across a common bug that occurs caused by clicking an api calling button multiple times. The issue is that the consecutive button click is causing repeated api calls. This causes unwanted operations in the database. The common solution is to disable the button before the api call and enable it after the call has been completed either by response or error.

The code looks like this

This solution works but this disabling and enabling needs to be added everywhere when a button calls api. The same code is being repeated everywhere for a click on button. But If the same code is repeated we should always look to extract it to a common place and decrease the clutter on the rendering part. For Angular there is an easy way to extract it to a directive and clean up the components code

import { Directive, ElementRef, Input } from '@angular/core';
import { EMPTY, exhaustMap, finalize, fromEvent, isObservable, Observable, of, Subscription, take } from 'rxjs';

export type ButtonHandler = (
  e?: MouseEvent
) => Observable<unknown> | Promise<unknown>;

const defaultHandler: ButtonHandler = (e) => EMPTY;
@Directive({
  selector: 'button[save-btn]',
  exportAs: 'saveBtn',
  host: {
    '[disabled]': ' _processing',
    '[class.loading]': '_processing',
  },
})
export class ButtonClickDirective {

  private _processing = false;
  private _sub = Subscription.EMPTY;

  @Input()
  handler: ButtonHandler = defaultHandler;

  get processing(): boolean {
    return this._processing;
  }

  constructor(private readonly btnElement: ElementRef<HTMLButtonElement>) {}

  ngAfterViewInit() {
    this._sub = fromEvent<MouseEvent>(this.btnElement.nativeElement, 'click')
      .pipe(exhaustMap((e) => this.wrapHandlerInObservable(e)))
      .subscribe();
  }

  ngOnDestroy() {
    this._sub.unsubscribe();
  }

  private wrapHandlerInObservable(e: MouseEvent) {
    this._processing = true;
    const handleResult = this.handler(e);
    let obs: Observable<unknown>;
    if (isObservable(handleResult)) {
      obs = handleResult;
    } else {
      obs = of(handleResult);
    }
    return obs.pipe(
      take(1),
      finalize(() => (this._processing = false))
    );
  }

}

Enter fullscreen mode Exit fullscreen mode

Here the directive uses a fromEvent operator from RxJs to create an event on button click. The components should use this operator to chain to this event using pipe operator and call the necessary api calls. The directive has a check using exhaustmap operator that make the check if an inner observable is active. If there is an inner observable, a flag is set to true. Based on this flag value we can add visual changes like disabling button or adding a spinner to the event target which is of course the button

handler: any = (e:any) => {
    const api = this.mockService.btnclickCheck();
    api.subscribe((res) => {
      console.log(res);
    });
    return api;
  };
Enter fullscreen mode Exit fullscreen mode
<button save-btn [handler]="handler">Click me!</button>
Enter fullscreen mode Exit fullscreen mode

The api calls required to be called are added as input. In the directive empty observable is set as default value for handler input. The response from api is handled at the end such that it is always an observable. The flag is reset at the end in finalize operator. You can find the sample code here https://github.com/tonyshaji/button-click-directive
Now the button and it’s api call is in your component, but disabling and enabling of button or adding some spinners for that cause is moved to a directive. Making the component leaner and cleaner

Top comments (0)