【问题标题】:Detect click outside Angular component检测 Angular 组件外部的点击
【发布时间】:2017-02-27 15:42:29
【问题描述】:

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

【问题讨论】:

标签: html events angular typescript


【解决方案1】:

解决方案

获取所有父母

var paths       = event['path'] as Array<any>;

检查是否有父组件是组件

var inComponent = false;    
paths.forEach(path => {
    if (path.tagName != undefined) {
        var tagName = path.tagName.toString().toLowerCase();
        if (tagName == 'app-component')
            inComponent = true;
    }
});

如果您将组件作为父组件,则在组件内部单击

if (inComponent) {
    console.log('clicked inside');
}else{
    console.log('clicked outside');
}

完整方法

@HostListener('document:click', ['$event'])
clickout(event: PointerEvent) {
    
    var paths       = event['path'] as Array<any>;
    
    var inComponent = false;    
    paths.forEach(path => {
        if (path.tagName != undefined) {
            var tagName = path.tagName.toString().toLowerCase();
            if (tagName == 'app-component')
                inComponent = true;
        }
    });
    
    if (inComponent) {
        console.log('clicked inside');
    }else{
        console.log('clicked outside');
    }
    
}

【讨论】:

    【解决方案2】:

    MVP的替代方案,只需要关注Event

    @HostListener('focusout', ['$event'])
      protected onFocusOut(event: FocusEvent): void {
        console.log(
          'click away from component? :',
          event.currentTarget && event.relatedTarget
        );
      }
    

    【讨论】:

      【解决方案3】:

      使用 event.stopPropagation()

      的另一种可能的解决方案
      1. 在最顶层的父组件上定义一个点击监听器,它会清除 click-inside 变量
      2. 在子组件上定义一个点击监听器,它首先调用 event.stopPropagation(),然后设置 click-inside 变量

      【讨论】:

        【解决方案4】:

        改进@J。科学怪人的回答

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

        【讨论】:

        • Niece 解决方案,但stopPropagation 可能会影响组件外部的逻辑:谷歌分析、关闭另一个组件等
        【解决方案5】:

        你可以调用像 (focusout) 或 (blur) 这样的事件函数,然后你输入你的代码

        <div tabindex=0 (blur)="outsideClick()">raw data </div>
         
        
         outsideClick() {
          alert('put your condition here');
           }
        

        【讨论】:

          【解决方案6】:

          ginalx's answer 应该设置为 imo 的默认值:此方法允许进行许多优化。

          问题

          假设我们有一个项目列表,并且我们希望在每个项目上都包含一个需要切换的菜单。我们在一个按钮上添加了一个切换按钮,该按钮在其自身(click)="toggle()" 上侦听click 事件,但我们也希望在用户点击菜单外部时切换菜单。如果项目列表增加并且我们在每个菜单上附加@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(() =&gt; !this.isActive), switchMap(() =&gt; this._utilitiesService.documentClickedTarget), filter((target) =&gt; !this._elementRef.nativeElement.contains(target)), tap(() =&gt; this._toggleMenuSubject$.next(false))。这样你就可以利用更多的 RxJs 并跳过一些订阅。
          • 也许你可以在这里帮助我 - stackoverflow.com/questions/69886444/…
          【解决方案7】:

          通过@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
            }
          

          【讨论】:

          • 我认为这个应该成为公认的答案,因为它允许许多优化:就像this example
          • 这是我在网上找到的最漂亮的解决方案
          • @lampshade 正确。我谈到了这个。再读一遍答案。我将取消订阅实现留给您的风格(takeUntil()、Subscription.add())。不要忘记退订!
          • @ginalx 我实现了您的解决方案,它按预期工作。虽然在我使用它的方式上遇到了问题。 Here's问题,请看
          • 有没有办法让 HostListener 仅在有订阅者对事件感兴趣时才处于活动状态?
          【解决方案8】:

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

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

          希望这会对您有所帮助。纠正我如果错过了什么。

          【讨论】:

            【解决方案9】:
            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';
              }
            }
            

            A working example - click here

            【讨论】:

            • 当触发元素中有一个由 ngIf 控制的元素时,这不起作用,因为 ngIf 从 DOM 中删除元素发生在点击事件之前:plnkr.co/edit/spctsLxkFCxNqLtfzE5q?p=preview
            • 它是否适用于通过以下方式动态创建的组件: const factory = this.resolver.resolveComponentFactory(MyComponent); const elem = this.vcr.createComponent(factory);
            • 一篇关于这个主题的好文章:christianliebel.com/2016/05/…
            【解决方案10】:

            你可以使用https://www.npmjs.com/package/ng-click-outside包中的clickOutside()方法

            【讨论】:

            • 更新:此软件包已被弃用
            【解决方案11】:

            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 或动态更新
            • 如果点击位于内部的元素,是否保证 clickInside 会在 clickout 之前被调用?
            猜你喜欢
            • 1970-01-01
            • 2019-06-20
            • 2018-12-11
            • 2023-03-23
            • 1970-01-01
            • 1970-01-01
            • 2022-06-10
            • 1970-01-01
            相关资源
            最近更新 更多