Détecter le clic à l'extérieur du composant angulaire

Réponses:

187
import { Component, ElementRef, HostListener, Input } from '@angular/core';

@Component({
  selector: 'selector',
  template: `
    <div>
      {{text}}
    </div>
  `
})
export class AnotherComponent {
  public text: String;

  @HostListener('document:click', ['$event'])
  clickout(event) {
    if(this.eRef.nativeElement.contains(event.target)) {
      this.text = "clicked inside";
    } else {
      this.text = "clicked outside";
    }
  }

  constructor(private eRef: ElementRef) {
    this.text = 'no clicks yet';
  }
}

Un exemple de travail - cliquez ici

AMagyar
la source
13
Cela ne fonctionne pas lorsqu'il y a un élément contrôlé par un ngIf à l'intérieur de l'élément déclencheur, car le ngIf supprimant l'élément du DOM se produit avant l'événement de clic: plnkr.co/edit/spctsLxkFCxNqLtfzE5q?p=preview
J. Frankenstein
fonctionne-t-il sur un composant qui a créé dynamiclly via: const factory = this.resolver.resolveComponentFactory (MyComponent); const elem = this.vcr.createComponent (usine);
Avi Moraly
1
Un bel article sur ce sujet: christianliebel.com/2016/05/...
Miguel Lara
47

Une alternative à la réponse d'AMagyar. Cette version fonctionne lorsque vous cliquez sur un élément qui est supprimé du DOM avec un ngIf.

http://plnkr.co/edit/4mrn4GjM95uvSbQtxrAS?p=preview

  private wasInside = false;
  
  @HostListener('click')
  clickInside() {
    this.text = "clicked inside";
    this.wasInside = true;
  }
  
  @HostListener('document:click')
  clickout() {
    if (!this.wasInside) {
      this.text = "clicked outside";
    }
    this.wasInside = false;
  }

J. Frankenstein
la source
Cela fonctionne parfaitement avec les mises à jour ngif ou dynamiques aussi
Vikas Kandari
C'est génial
Vladimir Demirev
24

La liaison au clic de document via @Hostlistener est coûteuse. Il peut avoir et aura un impact visible sur les performances si vous en abusez (par exemple, lors de la création d'un composant de liste déroulante personnalisé et que vous avez plusieurs instances créées dans un formulaire).

Je suggère d'ajouter un @Hostlistener () à l'événement de clic sur le document une seule fois dans le composant principal de votre application. L'événement doit pousser la valeur de l'élément cible sur lequel l'utilisateur a cliqué dans un sujet public stocké dans un service utilitaire global.

@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {

  constructor(private utilitiesService: UtilitiesService) {}

  @HostListener('document:click', ['$event'])
  documentClick(event: any): void {
    this.utilitiesService.documentClickedTarget.next(event.target)
  }
}

@Injectable({ providedIn: 'root' })
export class UtilitiesService {
   documentClickedTarget: Subject<HTMLElement> = new Subject<HTMLElement>()
}

Quiconque est intéressé par l'élément cible cliqué doit s'abonner au sujet public de notre service utilitaires et se désabonner lorsque le composant est détruit.

export class AnotherComponent implements OnInit {

  @ViewChild('somePopup', { read: ElementRef, static: false }) somePopup: ElementRef

  constructor(private utilitiesService: UtilitiesService) { }

  ngOnInit() {
      this.utilitiesService.documentClickedTarget
           .subscribe(target => this.documentClickListener(target))
  }

  documentClickListener(target: any): void {
     if (this.somePopup.nativeElement.contains(target))
        // Clicked inside  
     else
        // Clicked outside
  }
ginalx
la source
4
Je pense que celle-ci devrait devenir la réponse acceptée car elle permet de nombreuses optimisations: comme dans cet exemple
edoardo849
c'est la plus jolie solution que j'ai
eue
1
@lampshade Correct. J'en ai parlé. Relisez la réponse. Je laisse l'implémentation de désabonnement à votre style (takeUntil (), Subscription.add ()). N'oubliez pas de vous désinscrire!
ginalx
@ginalx J'ai implémenté votre solution, cela fonctionne comme prévu. Bien que rencontré un problème dans la façon dont je l'utilise. Voici la question, s'il vous plaît jeter un oeil
Nilesh
6

Les réponses mentionnées ci-dessus sont correctes, mais que se passe-t-il si vous effectuez un processus difficile après avoir perdu l'attention du composant concerné. Pour cela, je suis venu avec une solution avec deux indicateurs où le processus d'événement de mise au point n'aura lieu que lors de la perte du focus du composant pertinent uniquement.

isFocusInsideComponent = false;
isComponentClicked = false;

@HostListener('click')
clickInside() {
    this.isFocusInsideComponent = true;
    this.isComponentClicked = true;
}

@HostListener('document:click')
clickout() {
    if (!this.isFocusInsideComponent && this.isComponentClicked) {
        // do the heavy process

        this.isComponentClicked = false;
    }
    this.isFocusInsideComponent = false;
}

J'espère que ceci vous aidera. Corrigez-moi si vous avez manqué quelque chose.

Rishanthakumar
la source
2

La réponse de ginalx doit être définie par défaut imo: cette méthode permet de nombreuses optimisations.

Le problème

Disons que nous avons une liste d'éléments et que sur chaque élément, nous voulons inclure un menu qui doit être basculé. Nous incluons une bascule sur un bouton qui écoute un clickévénement sur lui (click)="toggle()"- même , mais nous voulons également basculer le menu chaque fois que l'utilisateur clique en dehors de celui-ci. Si la liste des éléments s'allonge et que nous attachons un @HostListener('document:click')à chaque menu, alors chaque menu chargé dans l'élément commencera à écouter le clic sur tout le document, même lorsque le menu est désactivé. Outre les problèmes de performances évidents, cela n'est pas nécessaire.

Vous pouvez, par exemple, vous abonner chaque fois que le popup est basculé via un clic et commencer à écouter les "clics extérieurs" seulement alors.


isActive: boolean = false;

// to prevent memory leaks and improve efficiency, the menu
// gets loaded only when the toggle gets clicked
private _toggleMenuSubject$: BehaviorSubject<boolean>;
private _toggleMenu$: Observable<boolean>;

private _toggleMenuSub: Subscription;
private _clickSub: Subscription = null;


constructor(
 ...
 private _utilitiesService: UtilitiesService,
 private _elementRef: ElementRef,
){
 ...
 this._toggleMenuSubject$ = new BehaviorSubject(false);
 this._toggleMenu$ = this._toggleMenuSubject$.asObservable();

}

ngOnInit() {
 this._toggleMenuSub = this._toggleMenu$.pipe(
      tap(isActive => {
        logger.debug('Label Menu is active', isActive)
        this.isActive = isActive;

        // subscribe to the click event only if the menu is Active
        // otherwise unsubscribe and save memory
        if(isActive === true){
          this._clickSub = this._utilitiesService.documentClickedTarget
           .subscribe(target => this._documentClickListener(target));
        }else if(isActive === false && this._clickSub !== null){
          this._clickSub.unsubscribe();
        }

      }),
      // other observable logic
      ...
      ).subscribe();
}

toggle() {
    this._toggleMenuSubject$.next(!this.isActive);
}

private _documentClickListener(targetElement: HTMLElement): void {
    const clickedInside = this._elementRef.nativeElement.contains(targetElement);
    if (!clickedInside) {
      this._toggleMenuSubject$.next(false);
    }    
 }

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

Et, dans *.component.html:


<button (click)="toggle()">Toggle the menu</button>
edoardo849
la source
Bien que je sois d'accord avec votre façon de penser, je suggérerais de ne pas insérer toute la logique dans un tapopérateur. Utilisez plutôt skipWhile(() => !this.isActive), switchMap(() => this._utilitiesService.documentClickedTarget), filter((target) => !this._elementRef.nativeElement.contains(target)), tap(() => this._toggleMenuSubject$.next(false)). De cette façon, vous utilisez beaucoup plus de RxJ et sautez certains abonnements.
Gizrah
0

Améliorer @J. Frankenstein answear

  
  @HostListener('click')
  clickInside($event) {
    this.text = "clicked inside";
    $event.stopPropagation();
  }
  
  @HostListener('document:click')
  clickout() {
      this.text = "clicked outside";
  }

John Libes
la source
-1

vous pouvez appeler la fonction d'événement comme (focusout) ou (flou) puis vous mettez votre code

<div tabindex=0 (blur)="outsideClick()">raw data </div>
 

 outsideClick() {
  alert('put your condition here');
   }
Rahul Yadav
la source