Angularコンポーネントの外側のクリックを検出


回答:


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

実際の例-ここをクリック


13
トリガー要素内にngIfによって制御される要素がある場合、これは機能しません。ngIfが要素をDOMから削除するのは、クリックイベントの前に発生するためです:plnkr.co/edit/spctsLxkFCxNqLtfzE5q?p
J. Frankenstein

次のようにして動的に作成されたコンポーネントで機能しますか:const factory = this.resolver.resolveComponentFactory(MyComponent); const elem = this.vcr.createComponent(factory);
Avi Moraly

1
このトピックに関する素晴らしい記事:christianliebel.com/2016/05/...
ミゲル・ララ

47

AMagyarの答えの代替案。このバージョンは、ngIfを使用してDOMから削除される要素をクリックすると機能します。

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


これはngifまたは動的更新でも完全に機能します
Vikas Kandari

これはすごい
Vladimir Demirev

23

@Hostlistenerを介したドキュメントクリックへのバインドはコストがかかります。使いすぎた場合(たとえば、カスタムドロップダウンコンポーネントを作成していて、フォームに複数のインスタンスを作成している場合)は、目に見えるパフォーマンスへの影響があります。

@Hostlistener()をメインアプリコンポーネント内で一度だけドキュメントクリックイベントに追加することをお勧めします。イベントは、クリックされたターゲット要素の値を、グローバルユーティリティサービスに格納されているパブリックサブジェクト内にプッシュする必要があります。

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

クリックされたターゲット要素に関心がある人は、ユーティリティサービスのパブリックサブジェクトにサブスクライブし、コンポーネントが破棄されたときにサブスクライブを解除する必要があります。

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
これは多くの最適化を可能にするため、受け入れられる答えになるはずだと思います。この例の
edoardo849

これは私がインターネットで手に入れた最もきれいなソリューションです
Anup Bangale

1
@lampshade正解。私はこれについて話しました。もう一度答えを読んでください。購読解除の実装はあなたのスタイルに任せます(takeUntil()、Subscription.add())。購読を解除することを忘れないでください!
ginalx

@ginalx私はあなたのソリューションを実装しました、それは期待通りに機能します。私がそれを使用する方法に問題が発生しましたが。こちらが質問です。ご覧ください
Nilesh

6

上記の答えは正しいですが、関連するコンポーネントからフォーカスを失った後に重いプロセスを実行している場合はどうでしょうか。そのため、関連するコンポーネントからのみフォーカスを失ったときにのみフォーカスアウトイベントプロセスが実行される2つのフラグを持つソリューションが用意されました。

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

これがお役に立てば幸いです。何かを逃してしまったら訂正してください。



2

ginalxの答えは、デフォルトのimoとして設定する必要があります。この方法では、多くの最適化が可能です。

問題

アイテムのリストがあり、トグルする必要があるメニューを含めるすべてのアイテムがあるとします。clickそれ自体のイベントをリッスンするボタンにトグルを含め(click)="toggle()"ますが、ユーザーがメニューの外側をクリックするたびにメニューをトグルしたいとします。アイテムのリストが大きくなり@HostListener('document:click')、すべてのメニューにをアタッチすると、アイテム内にロードされたすべてのメニューは、メニューがオフに切り替えられている場合でも、ドキュメント全体のクリックのリスニングを開始します。明らかなパフォーマンスの問題以外に、これは不要です。

たとえば、ポップアップがクリックによって切り替わったときにサブスクライブし、そのときだけ「外部クリック」のリッスンを開始できます。


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

そして、中*.component.html


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

私はあなたの考えに同意しますが、tapオペレーターにすべてのロジックを詰め込まないことをお勧めします。代わりに、を使用してください skipWhile(() => !this.isActive), switchMap(() => this._utilitiesService.documentClickedTarget), filter((target) => !this._elementRef.nativeElement.contains(target)), tap(() => this._toggleMenuSubject$.next(false))。この方法では、RxJをより多く利用し、一部のサブスクリプションをスキップします。
ギズラ

0

@Jの改善。フランケンシュタイン

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


-1

(focusout)や(blur)などのイベント関数を呼び出して、コードを記述できます

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

 outsideClick() {
  alert('put your condition here');
   }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.