Cory Rylan

My name is , Google Developer Expert, Speaker, Software Developer. Building Design Systems and Web Components.

Follow @coryrylan
Angular

Creating a Custom Debounce Click Directive in Angular

Cory Rylan

- 4 minutes

Updated

This article has been updated to the latest version Angular 17 and tested with Angular 16. The content is likely still applicable for all Angular 2 + versions.

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

The Directive API is a particular 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 an element is a 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() {}
}

An 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 let's 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 an Angular decorator called @HostListener. This decorator allows you to listen to events on the Host Element easily. 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';
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 an 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 wish 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 setup, 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 debounced by 500 milliseconds. After 500 milliseconds of no clicking, our Directive will emit the click event. Now we have the necessary functionality we need to do some cleanup work and add a little more functionality.

Unsubscribe

With RxJS Observables and Subject we must unsubscribe from the events once we are done 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, Subscription } from 'rxjs';
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 to 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 correctly. 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, Subscription } from 'rxjs';
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.

View Demo Code   
Twitter Facebook LinkedIn Email
 

No spam. Short occasional updates on Web Development articles, videos, and new courses in your inbox.

Related Posts

Angular

Creating Dynamic Tables in Angular

Learn how to easily create HTML tables in Angular from dynamic data sources.

Read Article
Web Components

Reusable Component Patterns - Default Slots

Learn about how to use default slots in Web Components for a more flexible API design.

Read Article
Web Components

Reusable Component Anti-Patterns - Semantic Obfuscation

Learn about UI Component API design and one of the common anti-patterns, Semantic Obfuscation.

Read Article