Klick außerhalb der Winkelkomponente erkennen

Antworten:

185
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';
  }
}

Ein funktionierendes Beispiel - hier klicken

AMagyar
quelle
12
Dies funktioniert nicht, wenn sich im Triggerelement ein Element befindet, das von einem ngIf gesteuert wird, da das Entfernen des Elements aus dem DOM durch ngIf
J. Frankenstein
Funktioniert es für eine Komponente, die dynamisch erstellt wurde über: const factory = this.resolver.resolveComponentFactory (MyComponent); const elem = this.vcr.createComponent (Fabrik);
Avi Moraly
Ein schöner Artikel zu diesem Thema: christianliebel.com/2016/05/…
Miguel Lara
46

Eine Alternative zu AMagyars Antwort. Diese Version funktioniert, wenn Sie auf ein Element klicken, das mit einer ngIf aus dem DOM entfernt wird.

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
quelle
Dies funktioniert perfekt mit ngif oder dynamischen Updates
Vikas Kandari
Das ist großartig
Vladimir Demirev
23

Das Binden an das Klicken von Dokumenten durch @Hostlistener ist kostspielig. Es kann und wird sichtbare Auswirkungen auf die Leistung haben, wenn Sie zu viel verwenden (z. B. beim Erstellen einer benutzerdefinierten Dropdown-Komponente und wenn mehrere Instanzen in einem Formular erstellt wurden).

Ich empfehle, dem Dokumentklickereignis nur einmal in Ihrer Haupt-App-Komponente einen @ Hostlistener () hinzuzufügen. Das Ereignis sollte den Wert des angeklickten Zielelements in ein öffentliches Subjekt verschieben, das in einem globalen Dienstprogrammdienst gespeichert ist.

@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>()
}

Wer sich für das angeklickte Zielelement interessiert, sollte das öffentliche Thema unseres Dienstprogrammdienstes abonnieren und sich abmelden, wenn die Komponente zerstört wird.

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
quelle
4
Ich denke, dass dies die akzeptierte Antwort sein sollte, da es viele Optimierungen ermöglicht: wie in diesem Beispiel
edoardo849
Dies ist die schönste Lösung, die ich im Internet bekommen habe
Anup Bangale
1
@ Lampenschirm Richtig. Ich habe darüber gesprochen. Lesen Sie die Antwort noch einmal. Ich überlasse die Abmeldeimplementierung Ihrem Stil (takeUntil (), Subscription.add ()). Vergessen Sie nicht, sich abzumelden!
Ginalx
@ginalx Ich habe Ihre Lösung implementiert, sie funktioniert wie erwartet. Obwohl ich auf ein Problem gestoßen bin, wie ich es benutze. Hier ist die Frage, bitte werfen Sie einen Blick darauf
Nilesh
6

Die oben genannten Antworten sind korrekt, aber was ist, wenn Sie einen schweren Prozess ausführen, nachdem Sie den Fokus der relevanten Komponente verloren haben? Dafür habe ich eine Lösung mit zwei Flags geliefert, bei der der Fokus-Out-Ereignisprozess nur stattfindet, wenn der Fokus nur von der relevanten Komponente verloren geht.

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;
}

Hoffe das wird dir helfen. Korrigieren Sie mich, wenn Sie etwas verpasst haben.

Rishanthakumar
quelle
2

Die Antwort von ginalx sollte als Standardantwort festgelegt werden: Diese Methode ermöglicht viele Optimierungen.

Das Problem

Angenommen, wir haben eine Liste mit Elementen, und zu jedem Element möchten wir ein Menü hinzufügen, das umgeschaltet werden muss. Wir fügen ein Umschalten auf eine Schaltfläche ein, die auf ein clickEreignis an sich wartet (click)="toggle()", aber wir möchten auch das Menü umschalten, wenn der Benutzer außerhalb davon klickt. Wenn die Liste der Elemente wächst und wir @HostListener('document:click')in jedem Menü ein Menü anhängen, wartet jedes im Element geladene Menü auf das Klicken auf das gesamte Dokument, auch wenn das Menü deaktiviert ist. Neben den offensichtlichen Leistungsproblemen ist dies nicht erforderlich.

Sie können beispielsweise abonnieren, wenn das Popup per Klick umgeschaltet wird, und erst dann auf "externe Klicks" warten.


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();
}

Und in *.component.html:


<button (click)="toggle()">Toggle the menu</button>
edoardo849
quelle
Obwohl ich Ihrer Meinung zustimme, würde ich vorschlagen, nicht die gesamte Logik in einen tapOperator zu stecken. Verwenden Sie stattdessen skipWhile(() => !this.isActive), switchMap(() => this._utilitiesService.documentClickedTarget), filter((target) => !this._elementRef.nativeElement.contains(target)), tap(() => this._toggleMenuSubject$.next(false)). Auf diese Weise nutzen Sie viel mehr RxJs und überspringen einige Abonnements.
Gizrah
0

@J verbessern. Frankenstein answear

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

John Libes
quelle