Reputation: 92347
In my template I have a field and two buttons:
<div class="btn-plus" (click)="add(1)"> - </div>
<div class="txt"> {{ myValue }} </div>
<div class="btn-minus" (click)="add(-1)"> + </div>
In my component .ts file I have:
add(num) {
this.myValue +=num;
this.update(); // async function which will send PUT request
}
The this.update()
function puts myValue
in the proper field in a big JSON object and sends it to a server.
Problem: When a user clicks 10x in a short period of time on button plus/minus, then a request will be send 10 times. But I want to send a request only once - 0.5 sec after last click. How to do it?
Upvotes: 11
Views: 23905
Reputation: 601
A simpler to understand option is to use a custom subject that emits on click and simply use rxjs native debounceTime
.
Live Example (open console logs):
Stackblitz
// declare these variables
clicker = new Subject();
clicker$ = this.clicker.asObservable();
//Add to ngOnInit(), change the number according to how sensitive you want to debounce
this.clicker$.pipe(debounceTime(200)).subscribe(() => {
console.log('Requesting Data ...');
this.service.getData().subscribe((d) => console.log(d));
});
// your button's (click) function:
this.clicker.next(true);
Upvotes: 0
Reputation: 9835
On Click event fir only first time so you don't need to wait for last click. OR how to ignore subsequent events ?
Solution by Ondrej Polesny on freecodecamp Website Also thanks to Cory Rylan for nice explanation about Debouncer
import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core';
@Directive({
selector: '[appDebounceClick]'
})
export class DebounceClickDirective {
@Input() debounceTime: number = 800;
@Output() debounceClick: EventEmitter<any> = new EventEmitter();
private onClickDebounce = this.debounce_leading(
(e) => this.debounceClick.emit(e), this.debounceTime
);
@HostListener('click', ['$event'])
clickEvent(event) {
event.preventDefault();
event.stopPropagation();
this.onClickDebounce(event);
}
private debounce_leading(func: any, timeout: number) {
let timer;
return (...args) => {
if (!timer) {
func.apply(this, args);
}
clearTimeout(timer);
timer = setTimeout(() => {
timer = undefined;
}, timeout);
};
};
}
Upvotes: 1
Reputation: 133
You can implement this with a setTimeout
if not want to use rxjs observable
instance. This would be an ideal implementation with memory leak cleanup on ngOnDestroy
:
@Component({
selector: "app-my",
templateUrl: "./my.component.html",
styleUrls: ["./my.component.sass"],
})
export class MyComponent implements OnDestroy {
timeoutRef: ReturnType<typeof setTimeout>;
clickCallback() {
clearTimeout(this.timeoutRef);
this.timeoutRef = setTimeout(()=> {
console.log('finally clicked!')
}, 500);
}
ngOnDestroy(): void {
clearTimeout(this.timeoutRef);
}
}
Edit: Updated timeoutRef TS def to safer one as suggested by @lord-midi on the comments.
Upvotes: 2
Reputation: 675
I ended up using a simplified version of the DebounceClickDirective
posted above. Since debounceTime
operator doesn't support leading/trailing options, I decided to use lodash
. This eliminates the delay from click to action, which in my case was opening a dialog and was pretty annoying.
Then I just use it like this <button (debounceClick)="openDialog()">
import { Directive, EventEmitter, HostListener, Output } from '@angular/core';
import { debounce } from 'lodash';
@Directive({
selector: 'button',
})
export class DebounceClickDirective {
@Output() debounceClick = new EventEmitter();
@HostListener('click', ['$event'])
debouncedClick = debounce(
(event: Event) => {
this.debounceClick.emit(event);
},
500,
{ leading: true, trailing: false },
);
}
Upvotes: 4
Reputation: 1139
A helper function --
export const debounced = (cb, time) => {
const db = new Subject();
const sub = db.pipe(debounceTime(time)).subscribe(cb);
const func = v => db.next(v);
func.unsubscribe = () => sub.unsubscribe();
return func;
};
Then an example use could be:
import { Component, OnInit } from '@angular/core';
import { debounced } from 'src/helpers';
@Component({
selector: 'app-example',
// Click calls `debouncedClick` instead of `myClick` directly
template: '<button (click)="debouncedClick($event)">Click This</button>'
})
export class Example implements OnDestroy {
debouncedClick; // Subject.next function
constructor() {
// Done in constructor or ngOnInit for `this` to resolve
this.debouncedClick = debounced($event => this.myClick($event), 800);
}
// Called after debounced resolves (800ms from last call)
myClick($event) {
console.log($event);
}
ngOnDestroy() {
// Stay clean!
this.debouncedFunc.unsubscribe();
}
}
Could also reverse the usage, calling the 'myClick' on click and have the debounced
callback perform the desired action. Dealer's choice.
Personally I this for (keyup)
events as well.
Unsure if the unsubscribe is really necessary - was quicker to implement than to research the memory leak :)
Upvotes: 2
Reputation: 92347
This is answer partially I found in internet, but I open to better solutions (or improve to below solution(directive)):
In internet I found appDebounceClick
directive which helps me in following way:
I remove update
from add
in .ts file:
add(num) {
this.myValue +=num;
}
And change template in following way:
<div
appDebounceClick
(debounceClick)="update()"
(click)="add(1)"
class="btn-plus"
> -
</div>
<div class="txt"> {{ myValue }} </div>
<!-- similar for btn-minus -->
Directive appDebounceClick
written by Cory Rylan (I put code here in case if link will stop working in future):
import { Directive, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Subscription } from 'rxjs/Subscription';
import { debounceTime } from 'rxjs/operators';
@Directive({
selector: '[appDebounceClick]'
})
export class DebounceClickDirective implements OnInit, OnDestroy {
@Input() debounceTime = 500;
@Output() debounceClick = new EventEmitter();
private clicks = new Subject();
private subscription: Subscription;
constructor() { }
ngOnInit() {
this.subscription = this.clicks.pipe(
debounceTime(this.debounceTime)
).subscribe(e => this.debounceClick.emit(e));
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
@HostListener('click', ['$event'])
clickEvent(event) {
event.preventDefault();
event.stopPropagation();
this.clicks.next(event);
}
}
Upvotes: 8
Reputation:
Use the takeUntil
operator :
export class AppComponent {
name = 'Angular';
calls = new Subject();
service = {
getData: () => of({ id: 1 }).pipe(delay(500)),
};
click() {
this.calls.next(true);
this.service.getData().pipe(
takeUntil(this.calls),
).subscribe(res => console.log(res));
}
}
Stackblitz (open your console to check the logs)
Upvotes: 9