Stop the horrible clash between single and double clicks in Angular

Stop the horrible clash between single and double clicks in Angular
Photo by Drew Hays / Unsplash

Have you ever wanted your app to behave differently on single-clicking and double-clicking? Sounds like an easy one to implement right? Wrong!

The issue is that the browser also interprets a double click as two single clicks and causes BOTH your event handlers to fire causing bloodshed and destruction 😭

We can neatly solve this problem by using the power of RxJs Subjects and preventing event bubbling using an ergonomic directive:

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

@Directive({
  selector: '[click.single],[click.double]',
})
export class ClickDoubleDirective implements OnInit, OnDestroy {
  @Input() debounceTime = 300;
  @Output('click.double') doubleClick = new EventEmitter();
  @Output('click.single') singleClick = new EventEmitter();

  private clicksSubject = new Subject<MouseEvent>();
  private subscription: Subscription;

  constructor() {}

  ngOnInit() {
    this.subscription = this.clicksSubject.pipe(debounceTime(this.debounceTime)).subscribe(event => {
      if (event.type === 'click') {
        this.singleClick.emit(event);
      } else {
        this.doubleClick.emit(event);
      }
    });
  }

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

  @HostListener('click', ['$event'])
  clickEvent(event: MouseEvent) {
    event.preventDefault();
    event.stopPropagation();
    this.clicksSubject.next(event);
  }
  @HostListener('dblclick', ['$event'])
  doubleClickEvent(event: MouseEvent) {
    event.preventDefault();
    event.stopPropagation();
    this.clicksSubject.next(event);
  }
} 
click-double.directive.ts

Explanation:

  • We have moved our click handling strategy into a directive so that we can reuse it easily on any DOM node in our Angular template
  • We have declared a Subject to hold the stream of our click events
  • By binding to both events using HostListener the directive, we prevent the bubbling and default behaviour of these events and pump the event into our clicks subject
  • The problem is that both click and dblclick events bubble upwards so on double-clicking, the single click handler will fire twice and then the double click handler will fire once. We need to stop this bubbling!
  • The browser specs guarantee the order of the dblclick event always occurring AFTER the click event. So, by using the debounceTime operator we can control the “speed” of the double-clicking. This will help us differentiate between click and dblclickevents. After the debounce window, if the value emitted by the clicks subject is not a double click,   then it is definitely a single click.
  • The `selector` of this directive also matches the output event names. This makes our usage API ergonomic and succinct as seen below:
// In some angular template bind to events `click.single` and `click.double`

<div (click.single)="onSingleClick($event)" (click.double)="onDoubleClick(event)">
  I got 99 problems but the click ain't one
</div>

And we’re done 🎉🎉🎉

Hope you found this useful

Happy Engineering!