我有一个按钮,可以在键盘输入上进行API调用。如果他们立即输入多次,它将为“ n”。通话次数。
如何使用干净的通用解决方案避免这种情况,以便可以在任何地方使用它?
<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);
}));
}
答案 0 :(得分:1)
我能想到的最完备的方法是使用指令扩展按钮元素的功能。
这个想法是按钮可以将其click事件映射到内部流中,并忽略所有后续的click事件,直到内部流完成为止。
这可以如下进行:
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));
}
}
您可以让他们将其用作:
<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);
}
中找到实时演示
答案 1 :(得分:0)
有两种解决方案:
在执行呼叫时禁用该按钮:
<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);
});
}
}
跳过多余的呼叫:
在执行前一个请求时,可以使用exhaustMap跳过请求。请注意,其他答案中建议的switchMap
和shareReplay
不会阻止过多的http调用。
<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);
});
}
}
请注意,当您按下click
键时也会触发enter
事件,因此不必监听“ 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')
答案 2 :(得分:0)
您可以添加一个指令,该指令在给定的时间内禁用按钮。
// 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)
}
}
// component.ts
<button [appDebounce]="1000" click="initiateBulkPayment()(keyup.enter)="initiateBulkPayment">
查看此live demo
答案 3 :(得分:-1)
您可以禁用/启用按钮以防止单击事件或使用shareReplay rxjs运算符
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);
}));
文档链接:https://www.learnrxjs.io/operators/multicasting/sharereplay.html