Detectar clique fora do componente angular

Respostas:

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

Um exemplo prático - clique aqui

AMagyar
fonte
13
Isso não funciona quando há um elemento controlado por um ngIf dentro do elemento acionador, uma vez que a remoção do elemento ngIf do DOM acontece antes do evento de clique: plnkr.co/edit/spctsLxkFCxNqLtfzE5q?p=preview
J. Frankenstein
funciona em um componente que foi criado dinamicamente via: const factory = this.resolver.resolveComponentFactory (MyComponent); const elem = this.vcr.createComponent (fábrica);
Avi Moraly
1
Um bom artigo sobre esse assunto: christianliebel.com/2016/05/…
Miguel Lara
47

Uma alternativa para a resposta de AMagyar. Esta versão funciona quando você clica no elemento que é removido do DOM com um 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
fonte
Isso funciona perfeitamente com ngif ou atualizações dinâmicas também
Vikas Kandari
Isso é incrível
Vladimir Demirev,
24

Vincular ao clique do documento por meio de @Hostlistener é caro. Ele pode e terá um impacto visível no desempenho se você usar excessivamente (por exemplo, ao construir um componente suspenso personalizado e tiver várias instâncias criadas em um formulário).

Sugiro adicionar um @Hostlistener () ao evento de clique do documento apenas uma vez dentro do componente principal do aplicativo. O evento deve empurrar o valor do elemento de destino clicado dentro de um assunto público armazenado em um serviço de utilidade 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>()
}

Quem estiver interessado no elemento alvo clicado deve se inscrever no assunto público do nosso serviço de utilitários e cancelar a inscrição quando o componente for destruído.

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
fonte
4
Acho que esta deve ser a resposta aceita, pois permite muitas otimizações: como neste exemplo
edoardo849 01 de
esta é a solução mais bonita que eu tenho na internet
Anup Bangale
1
@lampshade Correto. Eu falei sobre isso. Leia a resposta novamente. Deixo a implementação de cancelamento de assinatura para o seu estilo (takeUntil (), Subscription.add ()). Não se esqueça de cancelar a assinatura!
ginalx
@ginalx Implementei sua solução, ela funciona conforme o esperado. Embora tenha encontrado um problema na forma como o uso. Aqui está a pergunta, por favor, dê uma olhada
Nilesh
6

As respostas mencionadas acima estão corretas, mas e se você estiver fazendo um processo pesado depois de perder o foco do componente relevante. Para isso, eu vim com uma solução com dois sinalizadores onde o processo do evento focus out só ocorrerá quando perder o foco do componente relevante apenas.

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 isso ajude você. Corrija-me se tiver perdido alguma coisa.

Rishanthakumar
fonte
2

A resposta do ginalx deve ser definida como a resposta padrão imo: este método permite muitas otimizações.

O problema

Digamos que temos uma lista de itens e em cada item queremos incluir um menu que precisa ser alternado. Incluímos uma alternância em um botão que escuta um clickevento em si (click)="toggle()", mas também queremos alternar o menu sempre que o usuário clicar fora dele. Se a lista de itens aumentar e anexarmos um @HostListener('document:click')em cada menu, todos os menus carregados no item começarão a ouvir o clique em todo o documento, mesmo quando o menu estiver desativado. Além dos problemas de desempenho óbvios, isso é desnecessário.

Você pode, por exemplo, se inscrever sempre que o pop-up for alternado por meio de um clique e começar a ouvir "cliques externos" somente então.


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, em *.component.html:


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

edoardo849
fonte
Embora eu concorde com sua maneira de pensar, sugiro não colocar toda a lógica em um tapoperador. Em vez disso, use skipWhile(() => !this.isActive), switchMap(() => this._utilitiesService.documentClickedTarget), filter((target) => !this._elementRef.nativeElement.contains(target)), tap(() => this._toggleMenuSubject$.next(false)). Desta forma, você utiliza muito mais RxJs e pula algumas assinaturas.
Gizrah
0

Melhorando o @J. Resposta de Frankenstein

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

John Libes
fonte
-1

você pode chamar a função de evento como (focusout) ou (blur) e depois colocar seu código

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

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