Rileva clic all'esterno del componente angolare


Risposte:


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 esempio funzionante: fare clic qui


13
Questo non funziona quando è presente un elemento controllato da un ngIf all'interno dell'elemento trigger, poiché la rimozione dell'elemento ngIf dal DOM avviene prima dell'evento click: plnkr.co/edit/spctsLxkFCxNqLtfzE5q?p=preview
J. Frankenstein

funziona su un componente creato in modo dinamico tramite: const factory = this.resolver.resolveComponentFactory (MyComponent); const elem = this.vcr.createComponent (factory);
Avi Moraly

1
Un bell'articolo su questo argomento: christianliebel.com/2016/05/…
Miguel Lara

47

Un'alternativa alla risposta di AMagyar. Questa versione funziona quando fai clic su un elemento che viene rimosso dal DOM con 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;
  }


Funziona perfettamente anche con ngif o aggiornamenti dinamici
Vikas Kandari

È fantastico
Vladimir Demirev

24

Vincolare al clic del documento tramite @Hostlistener è costoso. Può e avrà un impatto visibile sulle prestazioni se si utilizza in modo eccessivo (ad esempio, quando si crea un componente a discesa personalizzato e si hanno più istanze create in un modulo).

Suggerisco di aggiungere un @Hostlistener () all'evento di clic del documento solo una volta all'interno del componente principale dell'app. L'evento dovrebbe spingere il valore dell'elemento target cliccato all'interno di un soggetto pubblico memorizzato in un servizio di utilità globale.

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

Chiunque sia interessato all'elemento di destinazione cliccato deve iscriversi all'argomento pubblico del nostro servizio di utilità e annullare l'iscrizione quando il componente viene distrutto.

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
  }

4
Penso che questa dovrebbe diventare la risposta accettata in quanto consente molte ottimizzazioni: come in questo esempio
edoardo849

questa è la soluzione più
carina che

1
@lampshade Correct. Ne ho parlato. Leggi di nuovo la risposta. Lascio l'implementazione dell'annullamento dell'iscrizione al tuo stile (takeUntil (), Subscription.add ()). Non dimenticare di annullare l'iscrizione!
ginalx

@ginalx Ho implementato la tua soluzione, funziona come previsto. Anche se ho riscontrato un problema nel modo in cui lo uso. Ecco la domanda, dai un'occhiata
Nilesh,

6

Le risposte sopra menzionate sono corrette, ma cosa succede se stai eseguendo un processo pesante dopo aver perso l'attenzione dal componente pertinente. Per questo, ho fornito una soluzione con due flag in cui il processo dell'evento focus out avrà luogo solo quando si perde il focus solo dal componente rilevante.

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

Spero che questo ti possa aiutare. Correggimi se hai perso qualcosa.



2

la risposta di ginalx dovrebbe essere impostata come quella predefinita imo: questo metodo consente molte ottimizzazioni.

Il problema

Diciamo che abbiamo un elenco di elementi e su ogni elemento vogliamo includere un menu che deve essere attivato. Includiamo un interruttore su un pulsante che ascolta un clickevento su se stesso (click)="toggle()", ma vogliamo anche attivare il menu ogni volta che l'utente fa clic al di fuori di esso. Se l'elenco degli elementi cresce e alleghiamo una @HostListener('document:click')su ogni menu, allora ogni menu caricato all'interno dell'elemento inizierà ad ascoltare il clic sull'intero documento, anche quando il menu è disattivato. Oltre agli ovvi problemi di prestazioni, questo non è necessario.

Puoi, ad esempio, iscriverti ogni volta che il popup viene attivato tramite un clic e iniziare ad ascoltare i "clic esterni" solo allora.


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

E, in *.component.html :


<button (click)="toggle()">Toggle the menu</button>

Anche se sono d'accordo con il tuo modo di pensare, suggerirei di non inserire tutta la logica in un tapoperatore. Invece, usa skipWhile(() => !this.isActive), switchMap(() => this._utilitiesService.documentClickedTarget), filter((target) => !this._elementRef.nativeElement.contains(target)), tap(() => this._toggleMenuSubject$.next(false)). In questo modo utilizzi molti più RxJ e salti alcuni abbonamenti.
Gizrah

0

Migliorare @J. Frankenstein risponde

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


-1

puoi chiamare la funzione evento come (focusout) o (blur) quindi inserisci il tuo codice

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

 outsideClick() {
  alert('put your condition here');
   }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.