Açısal bileşen dışındaki tıklamayı algıla


Yanıtlar:


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

Çalışan bir örnek - buraya tıklayın


13
Bu, tetikleyici öğenin içinde bir ngIf tarafından kontrol edilen bir öğe olduğunda çalışmaz, çünkü öğeyi DOM'dan kaldırma, tıklama olayından önce gerçekleşir: plnkr.co/edit/spctsLxkFCxNqLtfzE5q?p=preview
J. Frankenstein

şu şekilde dinamik olarak oluşturulan bir bileşen üzerinde çalışıyor mu: const factory = this.resolver.resolveComponentFactory (MyComponent); const elem = this.vcr.createComponent (fabrika);
Avi Moraly

1
Bu konuyla ilgili güzel bir makale: christianliebel.com/2016/05/…
Miguel Lara

47

AMagyar'ın cevabına bir alternatif. Bu sürüm, bir ngIf ile DOM'dan kaldırılan öğeye tıkladığınızda çalışır.

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


Bu, ngif veya dinamik güncellemelerle de mükemmel çalışır
Vikas

Bu harika
Vladimir Demirev

24

@ Hostlistener aracılığıyla belge tıklamaya bağlanmak maliyetlidir. Aşırı kullanırsanız (örneğin, özel bir açılır pencere bileşeni oluştururken ve bir formda oluşturulmuş birden çok örneğiniz varsa) performans üzerinde gözle görülür bir etkisi olabilir ve olacaktır.

Ana uygulama bileşeninizin içinde yalnızca bir kez belge tıklama etkinliğine bir @Hostlistener () eklemenizi öneririm. Olay, tıklanan hedef öğenin değerini global bir yardımcı program hizmetinde depolanan herkese açık bir konu içinde itmelidir.

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

Tıklanan hedef öğeyle ilgilenen herkes, yardımcı programlar hizmetimizin genel konusuna abone olmalı ve bileşen yok edildiğinde abonelikten çıkmalıdır.

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
Birçok optimizasyona izin verdiği için bunun kabul edilen cevap olması gerektiğini düşünüyorum: bu örnekte olduğu
edoardo849

bu internette
bulduğum

1
@lampshade Doğru. Bunun hakkında konuştum. Cevabı tekrar oku. Abonelikten çıkma uygulamasını tarzınıza bırakıyorum (takeUntil (), Subscription.add ()). Aboneliğinizi iptal etmeyi unutmayın!
ginalx

@ginalx Çözümünüzü uyguladım, beklendiği gibi çalışıyor. Yine de kullandığım şekilde bir sorunla karşılaştım. İşte soru, lütfen bir göz atın
Nilesh

6

Yukarıda belirtilen cevaplar doğrudur, ancak ilgili bileşenden odak noktasını kaybettikten sonra ağır bir işlem yapıyorsanız ne olur? Bunun için, odaklanma olayının yalnızca ilgili bileşenden odak kaybedildiğinde gerçekleşeceği iki bayraklı bir çözümle geldim.

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

Umarım bu sana yardımcı olur. Bir şey kaçırdıysanız beni düzeltin.



2

ginalx'in cevabı varsayılan bir imo olarak ayarlanmalıdır: bu yöntem birçok optimizasyona izin verir.

Sorun

Bir öğe listemiz olduğunu ve her öğede geçiş yapması gereken bir menü eklemek istediğimizi varsayalım. clickKendi başına bir olayı dinleyen bir düğmeye bir geçiş ekliyoruz (click)="toggle()", ancak aynı zamanda kullanıcı dışını tıkladığında menüyü değiştirmek istiyoruz. Öğe listesi büyürse ve bir@HostListener('document:click') her menüye bir eklersek, öğeye yüklenen her menü, menü kapatıldığında bile tüm belgenin tıklanmasını dinlemeye başlayacaktır. Bariz performans sorunlarının yanı sıra, bu gereksizdir.

Örneğin, pop-up bir tıklama ile değiştirildiğinde abone olabilir ve ancak o zaman "dış tıklamaları" dinlemeye başlayabilirsiniz.


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

Ve içinde *.component.html:


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

Düşünme şeklinize katılıyorum, ancak tüm mantığı bir tapoperatöre yerleştirmemenizi öneririm . Bunun yerine kullanın skipWhile(() => !this.isActive), switchMap(() => this._utilitiesService.documentClickedTarget), filter((target) => !this._elementRef.nativeElement.contains(target)), tap(() => this._toggleMenuSubject$.next(false)). Bu şekilde çok daha fazla RxJ kullanır ve bazı abonelikleri atlarsınız.
Gizrah

0

@J geliştiriliyor. Frankenstein cevap

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


-1

u (odaklanma) veya (bulanıklaştırma) gibi olay işlevini çağırabilir ve ardından kodunuzu koyabilirsiniz

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

 outsideClick() {
  alert('put your condition here');
   }
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.