Édité le 19/01/2021 :

J'ai écrit un autre article avec une version amélioré de la directive : https://k49.fr.nf/ngx-outside-click-ou-comment-detecter-un-clic-hors-dun-element-en-mieux/

Article original du 05/01/2021 :

Ça arrive finalement assez souvent qu'on ait besoin de détecter un clic non pas sur un élément mais hors d'un élément. Ce n'est pas quelque chose qui est prévu nativement en Angular. J'ai donc créé une directive pour ça et je vous la partage de suite :

outside-click.directive.ts :

import { Directive, EventEmitter, Hostlistener, Output } from '@angular/core';

@Directive({
    selector: '[appOutsideClick], [outsideClick]',
})
export class OutsideClickDirective {
    @output() outsideClick = new EventEmitter<MouseEvent>();
    
    private wasClick = false;
    
    @HostListener('click')
    onInsideClick(): void {
        this.wasClick = true;
    }
    
    @HostListener('document:click', ['$event'])
    onOutsideClick(event: MouseEvent): void {
        if (!this.wasClick) {
            this.outsideClick.emit(event);
        }
        this.wasClick = false;
    }
}

outside-click.module.ts :

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';

import { OutsideClickDirective } from './outside-click.directive';

@Directive({
    declarations: [OutsideClickDirective],
    imports: [CommonModule],
    exports: [OutsideClickDirective],
})
export class OutsideClickModule {}

Dans un autre composant :

<div (outsideClick)="onOutsideClick($event)">...</div>

Comment ça fonctionne ?

Tout se base sur le principe de bubbling des événements JavaScript. L'idée c'est qu'au clic sur un élément, l'événement va être aussi envoyé à son parent et ainsi de suite jusqu'à l'élément body.

Ici ce que je fais c'est que j'écoute le clic sur l'élément sur lequel on utilise la directive outsideClick, puis quand celui-ci est appelé je le note. Lorsque l'événement arrive au document (à l'élément body) je peux savoir si on est passé dans l'élément sur lequel on a placé la directive. Si c'est le cas je ne fais rien à part réinitialiser le drapeau indiquant qu'on est passé dans l'élément ; Si à l'inverse le drapeau n'indique pas qu'on est passé dans l'élément, c'est qu'on a cliqué ailleurs, donc on émet l'événement outsideClick.

Je n'ai pas créé de paquet npm pour cette directive qui me semble tellement basique qu'il vaut mieux copier-coller le module et la directive quand on en a besoin 😉