Delayed hover event in Angular

Use case:

Display tooltip only after user has been hovering for some time already. If he leaves sooner, then do not display anything.

Technologies

Angular, RxJs, Ngx UntilDestroy

Authors

David Votrubec and Luďek Cakl

Implementation

The logic behind is that we combine two streams. One stream is for the mouseenter event, and the other is for mouseleave. Then we map the events to boolean values. True means that we want to emit event, false means cancel.

The magic is in the combination of merge() and switchMap(). Merge listens to both streams, and returns single boolean observable. If it is false, we just return false. If it is true, we emit after delay. If there comes another false value, the delayed observable will not fire.

To clean up, there is untilDestroyed().

import { Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { untilDestroyed } from 'ngx-take-until-destroy';
import { fromEvent, merge, of } from 'rxjs';
import { delay, map, switchMap } from 'rxjs/operators';

@Directive({
	// tslint:disable-next-line:directive-selector
	selector: '[delayed-hover]',
})
export class DelayedHoverDirective implements OnInit, OnDestroy {
	@Input()
	delay = 1500;

	@Output('delayed-hover') hoverEvent = new EventEmitter();

	constructor(private readonly element: ElementRef) {}

	ngOnInit() {
		const hide$ = fromEvent(this.element.nativeElement, 'mouseleave').pipe(map(_ => false));
		const show$ = fromEvent(this.element.nativeElement, 'mouseenter').pipe(map(_ => true));

		merge(hide$, show$)
			.pipe(
				untilDestroyed(this),
				switchMap(show => {
					if (!show) {
						return of(false);
					}
					return of(true).pipe(delay(this.delay));
				})
			)
			.subscribe(show => {
				if (show) {
					this.hoverEvent.emit();
				}
			});
	}

	ngOnDestroy() {}
}

How to use it

<li (delayed-hover)="showTooltip()" delay="1500" (mouseout)="hideTooltip()"> ...</li>