检测Angular组件外部的单击

AMa*_*yar 75 html events typescript angular

如何检测Angular中组件的点击?

AMa*_*yar 140

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';
  }
}
Run Code Online (Sandbox Code Playgroud)

Plunker工作示例 - 点击这里

  • 当触发元素中有一个由ngIf控制的元素时,这不起作用,因为从DOM中删除元素的ngIf发生在click事件之前:http://plnkr.co/edit/spctsLxkFCxNqLtfzE5q?p = preview (8认同)
  • 关于此主题的一篇好文章:https://christianliebel.com/2016/05/angular-2-a-simple-click-outside-directive/ (2认同)

gin*_*alx 44

通过@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>()
}
Run Code Online (Sandbox Code Playgroud)

对被点击的目标元素感兴趣的人应该订阅我们公用事业服务的公共主题,并在组件被销毁时取消订阅。

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
  }
Run Code Online (Sandbox Code Playgroud)

  • 我认为这个应该成为公认的答案,因为它允许许多优化:就像[这个例子](/sf/answers/4201041561/) (6认同)
  • @lampshade 正确。我谈到过这个。再次阅读答案。我将取消订阅的实现留给您的风格(takeUntil()、Subscription.add())。不要忘记取消订阅! (3认同)

J. *_*ein 32

替代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;
  }
Run Code Online (Sandbox Code Playgroud)

  • 如果单击是在位于内部的元素上完成的,是否可以保证 clickInside 会比 clickout 更早被调用? (3认同)

Ris*_*mar 6

上面提到的答案是正确的,但是如果您在失去相关组件的焦点后正在做一个繁重的过程怎么办。为此,我提出了一个带有两个标志的解决方案,其中焦点移出事件过程只会在仅从相关组件失去焦点时发生。

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;
}
Run Code Online (Sandbox Code Playgroud)

希望这会帮助你。纠正我 如果错过了什么。


edo*_*849 5

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

Run Code Online (Sandbox Code Playgroud)

并且,在*.component.html


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

Run Code Online (Sandbox Code Playgroud)

  • 虽然我同意你的想法,但我建议不要将所有逻辑填充到“tap”运算符中。相反,请使用 `skipWhile(() =&gt; !this.isActive)、switchMap(() =&gt; this._utilitiesService.documentClickedTarget)、filter((target) =&gt; !this._elementRef.nativeElement.contains(target)),点击(() =&gt; this._toggleMenuSubject$.next(false))`。这样您就可以利用更多的 RxJ 并跳过一些订阅。 (2认同)

Joh*_*bes 5

改进@J。弗兰肯斯坦回答

  
  @HostListener('click')
  clickInside($event) {
    this.text = "clicked inside";
    $event.stopPropagation();
  }
  
  @HostListener('document:click')
  clickOutside() {
      this.text = "clicked outside";
  }
Run Code Online (Sandbox Code Playgroud)