Stop the horrible clash between single and double clicks in Angular
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);
}
}
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
anddblclick
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 theclick
event. So, by using thedebounceTime
operator we can control the “speed” of the double-clicking. This will help us differentiate betweenclick
anddblclick
events. 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!