Deteksi klik di luar komponen Angular

Jawaban:

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

Contoh kerja - klik di sini

AMagyar
sumber
13
Ini tidak berfungsi saat ada elemen yang dikontrol oleh ngIf di dalam elemen pemicu, karena ngIf menghapus elemen dari DOM terjadi sebelum peristiwa klik: plnkr.co/edit/spctsLxkFCxNqLtfzE5q?p=preview
J. Frankenstein
apakah ini bekerja pada komponen yang dibuat secara dinamis melalui: const factory = this.resolver.resolveComponentFactory (MyComponent); const elem = this.vcr.createComponent (pabrik);
Avi Moraly
1
Artikel bagus tentang topik ini: christianliebel.com/2016/05/…
Miguel Lara
47

Alternatif jawaban AMagyar. Versi ini berfungsi saat Anda mengklik elemen yang dihapus dari DOM dengan 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
sumber
Ini berfungsi sempurna dengan pembaruan ngif atau dinamis juga
Vikas Kandari
Ini luar biasa
Vladimir Demirev
24

Mengikat ke dokumen klik melalui @Hostlistener itu mahal. Ini dapat dan akan memiliki dampak kinerja yang terlihat jika Anda menggunakannya secara berlebihan (misalnya, saat membuat komponen tarik-turun khusus dan Anda memiliki beberapa contoh yang dibuat dalam satu formulir).

Saya sarankan menambahkan @Hostlistener () ke acara klik dokumen hanya sekali di dalam komponen aplikasi utama Anda. Peristiwa tersebut harus mendorong nilai elemen target yang diklik di dalam subjek publik yang disimpan dalam layanan utilitas 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>()
}

Siapa pun yang tertarik dengan elemen target yang diklik harus berlangganan subjek publik dari layanan utilitas kami dan berhenti berlangganan saat komponen dimusnahkan.

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
sumber
4
Saya pikir yang ini harus menjadi jawaban yang diterima karena memungkinkan banyak pengoptimalan: seperti dalam contoh ini
edoardo849
ini adalah solusi tercantik yang saya dapatkan di internet
Anup Bangale
1
@lampu. Saya berbicara tentang ini. Baca jawabannya lagi. Saya menyerahkan implementasi berhenti berlangganan ke gaya Anda (takeUntil (), Subscription.add ()). Jangan lupa untuk berhenti berlangganan!
ginalx
@ginalx Saya menerapkan solusi Anda, itu berfungsi seperti yang diharapkan. Meskipun mengalami masalah cara saya menggunakannya. Inilah pertanyaannya, silakan lihat
Nilesh
6

Jawaban yang disebutkan di atas benar tetapi bagaimana jika Anda melakukan proses yang berat setelah kehilangan fokus dari komponen yang relevan. Untuk itu, saya datang dengan solusi dengan dua flag dimana proses focus out event hanya akan berlangsung ketika kehilangan fokus dari komponen yang relevan saja.

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

Semoga ini bisa membantu Anda. Koreksi saya Jika melewatkan sesuatu.

Rishanthakumar
sumber
2

Jawaban ginalx harus disetel sebagai default imo: metode ini memungkinkan banyak pengoptimalan.

Masalah

Katakanlah kita memiliki daftar item dan pada setiap item kita ingin menyertakan menu yang perlu diubah. Kami menyertakan sakelar pada tombol yang mendengarkan clickacara itu sendiri (click)="toggle()", tetapi kami juga ingin beralih menu setiap kali pengguna mengeklik di luarnya. Jika daftar item bertambah dan kami melampirkan @HostListener('document:click')di setiap menu, maka setiap menu yang dimuat di dalam item akan mulai mendengarkan klik pada seluruh dokumen, bahkan saat menu dimatikan. Selain masalah kinerja yang jelas, ini tidak perlu.

Anda dapat, misalnya, berlangganan setiap kali popup diubah melalui satu klik dan mulai mendengarkan "klik luar" hanya setelah itu.


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

Dan, di *.component.html:


<button (click)="toggle()">Toggle the menu</button>
edoardo849
sumber
Meskipun saya setuju dengan cara Anda berpikir, saya sarankan untuk tidak memasukkan semua logika ke dalam tapoperator. Sebagai gantinya, gunakan skipWhile(() => !this.isActive), switchMap(() => this._utilitiesService.documentClickedTarget), filter((target) => !this._elementRef.nativeElement.contains(target)), tap(() => this._toggleMenuSubject$.next(false)). Dengan cara ini Anda memanfaatkan lebih banyak RxJ dan melewati beberapa langganan.
Gizrah
0

Meningkatkan @J. Frankenstein menjawab

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

John Libes
sumber
-1

Anda dapat memanggil fungsi acara seperti (focusout) atau (blur) lalu Anda memasukkan kode Anda

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

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