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

Sentry blog image

How to reduce TTFB

In the past few years in the web dev world, we’ve seen a significant push towards rendering our websites on the server. Doing so is better for SEO and performs better on low-powered devices, but one thing we had to sacrifice is TTFB.

In this article, we’ll see how we can identify what makes our TTFB high so we can fix it.

Read more

Top comments (0)

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay