DEV Community

Tony Shaji
Tony Shaji

Posted on • Edited on

4

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

saveData(): void {
    this.isSaving = true; // Disable the button and show the spinner

    // Call the service method
    this.mockService.btnclickCheck().subscribe({
      next: (response) => {
        console.log('Service response:', response);
        this.isSaving = false; // Re-enable the button
      },
      error: (error) => {
        console.error('Error occurred:', error);
        this.isSaving = false; // Re-enable the button even on error
      }
    });
  }
Enter fullscreen mode Exit fullscreen mode

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

Image of Docusign

Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

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

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

Okay