How to prevent double click in Angular?

Téwa picture Téwa · Jul 17, 2018 · Viewed 24.7k times · Source

I have a component with click.

<my-box (click)="openModal()"></my-box>

When I click this element, openModal function will run. And I'd like to give 1000ms throttle time in order to prevent opening multiple modals.

My first approach was using Subject (from rxJs)

//html
<my-box (click)="someSubject$.next()"></my-box>
//ts
public someSubject$:Subject<any> = new Subject();
...etc subscribe

But I feel it's a bit verbose.

Next Approach was using a directive. I modified a bit of code that I found by googling.

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

@Directive({
    selector: '[noDoubleClick]'
})
export class PreventDoubleClickDirective {

    constructor() {
    }

    @HostListener('click', ['$event'])
    clickEvent(event) {
        event.stopPropagation();    // not working as I expected.
        event.preventDefault();     // not working as I expected.

        event.srcElement.setAttribute('disabled', true);    // it won't be working unless the element is input.
        event.srcElement.setAttribute('style', 'pointer-events: none;');   // test if 'pointer-events: none' is working but seems not. 

        setTimeout(function () {
            event.srcElement.removeAttribute('disabled');
        }, 500);
    }
}

//html
<my-box noDoubleClick (click)="openModal()"></my-box>

However, whatever I try, always openModal was executed. I couldn't find how to stop executing openModal in the directive.

I can just make like

//ts
//In the openModal method.
openModal() {
    public isClickable = true

    setTimeout(() => {
        this.newsClickable = true;
    }, 1000);
    ...
}

But for the reusable code, I think using directive is ideal.

How can I make it?

Answer

Sam Herrmann picture Sam Herrmann · Jul 17, 2018

You can use RxJs' debounce or debounceTime operator to prevent double clicks. Here is also a post on how to create a custom debounce click directive.

In case the post is taken down in the future, here is the final code:

Directive:

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

Example Usage:

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