Detectar clic fuera del componente angular

Respuestas:

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 ejemplo práctico: haga clic aquí

AMagyar
fuente
13
Esto no funciona cuando hay un elemento controlado por un ngIf dentro del elemento disparador, ya que el ngIf eliminar el elemento del DOM ocurre antes del evento click: plnkr.co/edit/spctsLxkFCxNqLtfzE5q?p=preview
J. Frankenstein
¿Funciona en un componente que se creó dinámicamente a través de: const factory = this.resolver.resolveComponentFactory (MyComponent); const elem = this.vcr.createComponent (fábrica);
Avi Moraly
1
Un buen artículo sobre este tema: christianliebel.com/2016/05/…
Miguel Lara
47

Una alternativa a la respuesta de AMagyar. Esta versión funciona cuando hace clic en el elemento que se elimina del 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;
  }

J. Frankenstein
fuente
Esto también funciona perfectamente con ngif o actualizaciones dinámicas
Vikas Kandari
Esto es increíble
Vladimir Demirev
23

La vinculación al clic del documento a través de @Hostlistener es costosa. Puede tener y tendrá un impacto visible en el rendimiento si lo abusa (por ejemplo, cuando crea un componente desplegable personalizado y tiene varias instancias creadas en un formulario).

Sugiero agregar un @Hostlistener () al evento de clic del documento solo una vez dentro de su componente principal de la aplicación. El evento debería empujar el valor del elemento objetivo en el que se hizo clic dentro de un sujeto público almacenado en un servicio de utilidad 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>()
}

Quien esté interesado en el elemento de destino en el que se hizo clic debe suscribirse al tema público de nuestro servicio de utilidades y darse de baja cuando se destruya el componente.

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
fuente
4
Creo que esta debería convertirse en la respuesta aceptada, ya que permite muchas optimizaciones: como en este ejemplo
edoardo849
esta es la solución más bonita que obtuve en Internet
Anup Bangale
1
@lampshade Correcto. Hablé de esto. Lee la respuesta de nuevo. Dejo la implementación de cancelación de suscripción a su estilo (takeUntil (), Subscription.add ()). ¡No olvides darte de baja!
ginalx
@ginalx Implementé su solución, funciona como se esperaba. Aunque encontré un problema por la forma en que lo uso. Aquí está la pregunta, por favor eche un vistazo
Nilesh
6

Las respuestas mencionadas anteriormente son correctas, pero ¿qué sucede si está haciendo un proceso pesado después de perder el enfoque del componente relevante? Para eso, vine con una solución con dos banderas donde el proceso de evento de enfoque solo se llevará a cabo cuando se pierda el enfoque solo del componente relevante.

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

Espero que esto te ayudará. Corrígeme si me he perdido algo.

Rishanthakumar
fuente
2

La respuesta de ginalx debe establecerse como la predeterminada imo: este método permite muchas optimizaciones.

El problema

Digamos que tenemos una lista de elementos y que en cada elemento queremos incluir un menú que debe alternarse. Incluimos una palanca en un botón que escucha un clickevento en sí mismo (click)="toggle()", pero también queremos cambiar el menú cada vez que el usuario hace clic fuera de él. Si la lista de elementos crece y adjuntamos un @HostListener('document:click')en cada menú, entonces cada menú cargado dentro del elemento comenzará a escuchar el clic en todo el documento, incluso cuando el menú está desactivado. Además de los problemas obvios de rendimiento, esto es innecesario.

Puede, por ejemplo, suscribirse cada vez que se activa la ventana emergente mediante un clic y comenzar a escuchar "clics externos" solo entonces.


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

Y en *.component.html:


<button (click)="toggle()">Toggle the menu</button>
edoardo849
fuente
Si bien estoy de acuerdo con su forma de pensar, sugeriría no meter toda la lógica en un tapoperador. En su lugar, utilice skipWhile(() => !this.isActive), switchMap(() => this._utilitiesService.documentClickedTarget), filter((target) => !this._elementRef.nativeElement.contains(target)), tap(() => this._toggleMenuSubject$.next(false)). De esta manera, utiliza muchos más RxJ y omite algunas suscripciones.
Gizrah
0

Mejorando @J. Frankenstein responde

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

John Libes
fuente
-1

Puedes llamar a la función de evento como (focusout) o (desenfoque) y luego pones tu código

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

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