最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Avoid multiple http calls on pressing enter button multiple times - Stack Overflow

programmeradmin1浏览0评论

I have a button which makes an API call on keyboard enter. If they enter instantly multiple times, it would make 'n' no. of calls.

How to avoid this with a clean generic solution, so that it can be used everywhere?

    <button click="initiateBulkPayment()(keyup.enter)="initiateBulkPayment">

    initiateBulkPayment = (orderId:any, payment_methods:any) => {
       let postParams = payment_methods;
       console.log('this is payment_method', payment_methods);


       return this.http.post(Constants.API_ENDPOINT + '/oms/api/orders/' + 
           orderId + '/payments/bulk_create/', postParams, this.auth.returnHeaderHandler())
          .pipe(map((data: any) => {
           return data;
       }),
       catchError((err)=>{
         return throwError(err);
       }));
   }

I have a button which makes an API call on keyboard enter. If they enter instantly multiple times, it would make 'n' no. of calls.

How to avoid this with a clean generic solution, so that it can be used everywhere?

    <button click="initiateBulkPayment()(keyup.enter)="initiateBulkPayment">

    initiateBulkPayment = (orderId:any, payment_methods:any) => {
       let postParams = payment_methods;
       console.log('this is payment_method', payment_methods);


       return this.http.post(Constants.API_ENDPOINT + '/oms/api/orders/' + 
           orderId + '/payments/bulk_create/', postParams, this.auth.returnHeaderHandler())
          .pipe(map((data: any) => {
           return data;
       }),
       catchError((err)=>{
         return throwError(err);
       }));
   }
Share Improve this question edited Aug 26, 2019 at 8:46 Harshad Vekariya 1,0521 gold badge8 silver badges28 bronze badges asked Aug 26, 2019 at 8:09 sharath subramanyasharath subramanya 1172 silver badges13 bronze badges 2
  • shareReply will allow you to multicast the response, but to prevent to actually call it, you have to have some mechanism outside and the isProccessed is the typical way. – Akxe Commented Aug 26, 2019 at 8:22
  • 2 cant you just disable the button when a request is being processed? – Jota.Toledo Commented Aug 26, 2019 at 8:23
Add a ment  | 

5 Answers 5

Reset to default 1

There are two solutions:

  1. Disable the button while the call is being executed
  2. Skip the excess calls

Disable the button while the call is being executed:

<button [disabled]="paymentRequest.inProgress$ | async" (click)="onPayButtonClick()">
export class ProgressRequest {
    private _inProgress$ = new UniqueBehaviorSubject(false);

    execute<TResult>(call: () => Observable<TResult>): Observable<TResult> {
        if (!this._inProgress$.value) {
            this._inProgress$.next(true);
            return call().pipe(
                finalize(() => {
                    this._inProgress$.next(false);
                })
            );
        } else {
            throw new Error("the request is currently being executed");
        }
    }

    get inProgress$(): Observable<boolean> {
        return this._inProgress$;
    }
}

@Component({ ... })
export class MyComponent {
    readonly paymentRequest = new ProgressRequest();

    onPayButtonClick() {
        this.paymentRequest.execute(() => {
            return this.http.post(
                Constants.API_ENDPOINT + '/oms/api/orders/' + orderId + '/payments/bulk_create/',
                postParams,
                this.auth.returnHeaderHandler()
            ).pipe(map((data: any) => {
                return data;
            });
        }).subscribe(data => {
            console.log("done!", data);
        });
    }
}

Skip the excess calls:

You can use exhaustMap to skip requests while the previoius one is being executed. Note that switchMap and shareReplay, which was suggested in other answers won't prevent excess http calls.

<button #paymentButton>
@Component({ ... })
export class MyComponent implements OnInit {
    @ViewChild('paymentButton', { static: true })
    readonly paymentButton!: ElementRef<HTMLElement>;

    ngOnInit() {
        merge(
            fromEvent(this.paymentButton.nativeElement, 'click'),
            fromEvent<KeyboardEvent>(this.paymentButton.nativeElement, 'keyup').pipe(
                filter(event => event.key === "Enter")
            )
        ).pipe(
            exhaustMap(() => {
                return this.http.post(
                    Constants.API_ENDPOINT + '/oms/api/orders/' + orderId + '/payments/bulk_create/',
                    postParams,
                    this.auth.returnHeaderHandler()
                ).pipe(map((data: any) => {
                    return data;
                });
            })
        ).subscribe(result => {
            console.log(result);
        });
    }
}

Note that click event is fired also when you press the enter key, so it isn't necessary to listen 'keyup'.

// You can replace
merge(
    fromEvent(this.paymentButton.nativeElement, 'click'),
    fromEvent<KeyboardEvent>(this.paymentButton.nativeElement, 'keyup').pipe(
        filter(event => event.key === "Enter")
    )
)

// just by
fromEvent(this.paymentButton.nativeElement, 'click')

The most self-contained approach that I could think of is using a directive to extend the functionality of a button element.

The idea is that the button can map its click event into an inner stream, and ignore all subsequent click events until the inner stream pletes.

This can be done as follows:

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

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

const defaultHandler: ButtonHandler = (e) => empty();

@Directive({
  selector: 'button[serial-btn]',
  exportAs: 'serialBtn',
  host: {
    '[disabled]': 'disableWhenProcessing && _processing'
  }
})
export class SerialButtonDirective implements AfterViewInit {
  private _processing = false;
  private _sub = Subscription.EMPTY;

  @Input()
  disableWhenProcessing = false;

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

You could them use it as:

<button serial-btn [handler]="handler">Handle</button>

import {timer} from 'rxjs';
import {ButtonHandle} from './directive-file';

handler: ButtonHandler = (e) => {
    console.log(e);
    return timer(3000);
  }

A live demo can be found in this stackblitz

Make an attribute directive firstly,you can set the time for blocking aswell,

 @Directive({
          selector: '[preventMultipleCalls]'
        })
        export class PreventApiCallsDirective implements OnInit,OnDestroy {
        
          @Input('time') throttleTimeValue = 10000 
          @Output('emit') fireCallEventEmitter :EventEmitter<boolean>= new EventEmitter<boolean>();
          clickObservable = new Observable<Event>();
          clickSubscription: Subscription;
        
          constructor(private elementRef: ElementRef) {
            this.clickObserable = fromEvent(this.elementRef.nativeElement,'keyup')
           }
        
          ngOnInit(): void {
            this.clickSubscription = this.clickObserable.pipe(filter((e :any) => e.keyCode === 13),
               throttleTime(this.throttleTimeValue))
              .subscribe(event => {
                this.fireCallEventEmitter.emit(true)
              });
          }
        
          ngOnDestroy(): void {
            this.clickSubscription?.unsubscribe();
          }
        
        }

And place that directive like this on the button:

<button preventMultipleCalls (emit)="initiateBulkPayment()"> </button>

You can add a Directive that disables the button for a given amount of time.

// debounce.directive.ts
import { Directive, OnInit, HostListener, ElementRef, Input } from '@angular/core';

@Directive({
  selector: '[appDebounce]'
})
export class DebounceDirective {
  constructor(
    private el: ElementRef<HTMLButtonElement>,
  ) { }

  @Input() appDebounce: number;

  @HostListener('click') onMouseEnter() {
    this.el.nativeElement.disabled = true;
    setTimeout(() => this.el.nativeElement.disabled = false, this.appDebounce)
  }
}

// ponent.ts
<button [appDebounce]="1000" click="initiateBulkPayment()(keyup.enter)="initiateBulkPayment">

See this live demo

You can disable/enable the button to prevent click event or use shareReplay rxjs operator

return this.http.post(Constants.API_ENDPOINT + '/oms/api/orders/' + 
         orderId + '/payments/bulk_create/', postParams, 
         this.auth.returnHeaderHandler())
       .pipe(map((data: any) => {
               return data;
            }
        ),
        shareReplay(1)
        catchError((err)=>{
          return throwError(err);
        }));

Docs link: https://www.learnrxjs.io/operators/multicasting/sharereplay.html

发布评论

评论列表(0)

  1. 暂无评论