This article is for versions of Angular 2, Angular 4, Angular 5 and later.

This article has been updated to use the new RxJS Lettable Operators.

In this post we will cover the Angular Directive API to create our own custom debounce click directive. This directive will handle denouncing multiple click events over a specified amount of time. This is useful to help prevent duplicate actions.

The Directive API is a special way to add behavior to existing DOM elements or components. For our use case we want to debounce or delay click events from occurring when a element is click. To do this we will cover concepts from the Directive API, HostListener API and RxJS.

First we need to create our Directive class and register it to our app.module.ts.



import { Directive, OnInit } from '@angular/core';

@Directive({
  selector: '[appDebounceClick]'
})
export class DebounceClickDirective implements OnInit {
  constructor() { }

  ngOnInit() { }
}


A Angular Directive is essentially a component without a template. The behavior defined our Directive class will be applied to the host element.



<button appDebounceClick>Debounced Click</button>


The Host Element in the markup above is our HTML button. The first thing we want to do is listen to when the Host Element is clicked. So lets add the following code to our directive.



import { Directive, HostListener, OnInit } from '@angular/core';

@Directive({
  selector: '[appDebounceClick]'
})
export class DebounceClickDirective implements OnInit {
  constructor() { }

  ngOnInit() { }

  @HostListener('click', ['$event'])
  clickEvent(event) {
    event.preventDefault();
    event.stopPropagation();
    console.log('Click from Host Element!');
  }
}


In our example above we are using a Angular decorator called @HostListener. This decorator allows you to easily listen to events on the Host Element. In our example the first parameter is the click event. The second parameter $event allows us to tell Angular to pass in the click event to our Directive method clickEvent(event).

With the click event we can call event.preventDefault(); and event.stopPropagation();. These two lines prevent the click event from bubbling up to the parent component. We want this behavior so we can control when the click event fires.

Debounce Events

Now that we can intercept the Host Element click event we need to have a way to debounce those events and then re-emit them back to the parent. To this we have two parts to implement, the Event Emitter and a RxJS Subject to debounce the Events.



import { Directive, EventEmitter, HostListener, OnInit, Output } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { debounceTime } from 'rxjs/operators';

@Directive({
  selector: '[appDebounceClick]'
})
export class DebounceClickDirective implements OnInit {
  @Output() debounceClick = new EventEmitter();
  private clicks = new Subject();

  constructor() { }

  ngOnInit() {
    this.clicks.pipe(
      debounceTime(500)
    ).subscribe(e => this.debounceClick.emit(e));
  }

  @HostListener('click', ['$event'])
  clickEvent(event) {
    event.preventDefault();
    event.stopPropagation();
    this.clicks.next(event);
  }
}


In the code above we are using a Angular Decorator @Output. The output Decorator with the EventEmitter class allows us to create custom event on DOM elements and components. To emit events we call the emit event on the Event Emitter instance.

We don’t want to emit the click event immediately we want to debounce or delay the event. To get this behavior we will use a RxJS class called a Subject. A Subject allows us to listen to events as well as emit them. In our code we create a subject to handle our click events. On our method we call .next() to have the Subject emit the next value. We can also use a special RxJS function operator called debounceTime. This allows us to debounce the event based on a given number of milliseconds on the Subject events.

Once we have this set up we can now listen to our custom debounce click event in our template like below.



<button appDebounceClick (debounceClick)="log()">Debounced Click</button>


Now when we click our button it is debounce by 500 milliseconds. After 500 milliseconds of no clicking our Directive will emit the click event. Now we have our basic functionality we need to do some clean up work and add a little more functionality.

Unsubscribe

With RxJS Observables and Subject we must unsubscribe from the events once we are dont listening to them. If we don’t we can accidentally create memory leaks.



import { Directive, EventEmitter, HostListener, Input, 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 {
  @Output() debounceClick = new EventEmitter();
  private clicks = new Subject();
  private subscription: Subscription;

  constructor() { }

  ngOnInit() {
    this.subscription = this.clicks.pipe(
      debounceTime(500)
    ).subscribe(e => this.debounceClick.emit(e));
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

  @HostListener('click', ['$event'])
  clickEvent(event) {
    event.preventDefault();
    event.stopPropagation();
    this.clicks.next(event);
  }
}


To unsubscribe we catch the subscription object that is returned when subscribing in a class property. When Angular destroys or removes the DOM element it will call the OnDestroy life cycle hook where we can unsubscribe from our Subject events.

Custom Inputs

Our directive is fully functional and handles events properly. Next we are going to add a little more logic to allow us to customize the debounce time whenever needed. To do this we will use the @Input decorator.



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


The @Input decorator allows us to pass data into our Components and Directives. In the code above we can take in a input to specify how long we would like the debounce time to be. By default we will set it to 500 milliseconds. With the @Input we can now set this value in our templates like below.



<button appDebounceClick (debounceClick)="log()" [debounceTime]="700">Debounced Click</button>


Feel free to check out the full working demo in the link below.

Demo