Angular - Creating a Debounce Directive(s)
Published on
A very common and useful directive that we can create is a simple debounce directive. If you’re not familiar with debouncing, it essentially will discard events or values until a specified time has ellapsed between the emission of those events/values. This is commonly used to prevent spam button clicks or spamming an API call from a autocomplete/search input.
We’re going to create two debounce directives, one to debounce key up events that will be used on input
elements and another to debounce button clicks.
A stackblitz example of our directives
Creating a debounce directive
Since we are creating multiple debounce directives to handle different events (keyup, click, etc), we will create a base class to hold all common/shared logic between the different events we are debouncing.
abstract-debounce.directive.ts
import { Directive, OnDestroy, Input, Output, EventEmitter } from "@angular/core";
import { Subject } from "rxjs";
import { takeUntil, debounceTime, distinctUntilChanged, tap } from "rxjs/operators";
@Directive()
export abstract class AbstractDebounceDirective implements OnDestroy {
@Input()
public debounceTime: number;
@Output()
public onEvent: EventEmitter<any>;
protected emitEvent$: Subject<any>;
protected subscription$: Subject<void>;
constructor() {
this.debounceTime = 500;
this.onEvent = new EventEmitter<any>();
this.emitEvent$ = new Subject<any>();
this.subscription$ = new Subject<void>();
}
ngOnInit(): void {
this.emitEvent$
.pipe(
takeUntil(this.subscription$),
debounceTime(this.debounceTime),
distinctUntilChanged(),
tap(value => this.emitChange(value))
)
.subscribe();
}
public emitChange(value: any): void {
this.onEvent.emit(value);
}
ngOnDestroy(): void {
this.subscription$.next();
this.subscription$.complete();
}
}
Our debounce directive will have two bindings, one @Input
binding to set the debounce time, and an @Output
to bind the callback when a debounced event is emitted.
Binding | Description |
---|---|
[debounceTime] |
The time in milliseconds to debounce event (default is 500ms) |
(onEvent) |
The output event callback after debouncing |
The key to our debounce directive is the Rxjs Subject
(emitEvent$
) which we will use to pipe our events/values with several Rxjs operators. So do our pipe operators do?
takeUntil
- This simply handles our subscription to our
emitEvent$
Subject. Our subscription will complete once thesubscription$
Subject emits value. This operator could ommited and our subscription could be handled with aSubscription
instead.
debounceTime
- This operator will debounce events/values based on the specified time, which in our case is our
@Input
value (defaulted to 500ms). So a value will not be emitted until the specified time has elapsed between events/values.
distinctUntilChanged
- This operator will discard duplicate consecutive events. Basically will not emit the value if the value hasn’t changed since the last emission.
tap
- This is where we will handle the value that should be emitted after being piped through
debounceTime
anddistinctUntilChanged
. This could also be handled in the subscription callback instead of using thetap
operator.
Debouncing a key up event
Now that we have a base logic in place for our debouncing, we can create directives to debounce different event types. Our first directive will debounce key up events. This can be useful for autocomplete/search inputs where you’re make an API call to get some resultset based on user input. The debounce will prevent the API call on every keyup but instead will call the API after the specified time between events is reached.
debounce-keyup.directive.ts
import { Directive, HostListener } from "@angular/core";
import { AbstractDebounceDirective } from "./abstract-debounce.directive";
@Directive({
selector: "input[fooDebounceKeyUp]"
})
export class DebounceKeyupDirective extends AbstractDebounceDirective {
constructor() {
super();
}
@HostListener("keyup", ["$event"])
public onKeyUp(event: any): void {
event.preventDefault();
this.emitEvent$.next(event);
}
}
Here we are simply using the @HostListener
decorator to listen to key up events then pushing them through our emitEvent$
Subject in our AbstactDebounceDirective
base class. This will pipe our event through the pipe operators and finally emit the event if it successfully passes through.
Debouncing a click event
Our DebounceClickDirective
will be identical to our DebounceKeyUpDirective
except that we will be listening to click events with the @HostListener
decorator instead of keyup events.
debounce-click.directive.ts
import { Directive, HostListener } from "@angular/core";
import { AbstractDebounceDirective } from "./abstract-debounce.directive";
@Directive({
selector: "[fooDebounceClick]"
})
export class DebounceClickDirective extends AbstractDebounceDirective {
constructor() {
super();
}
@HostListener("click", ["$event"])
public onKeyUp(event: any): void {
event.preventDefault();
this.emitEvent$.next(event);
}
}
Stackblitz Example
Below I’ve embedded a Stackblitz of our directives in action. The button will debounce click with a 2 second debounce time. The input will debounce key up event with a 1 second debounce time.