简体   繁体   English

检测 Angular 组件外的点击

[英]Detect click outside Angular component

How can I detect clicks outside a component in Angular?如何检测 Angular 中组件外部的点击?

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 一个工作示例 - 点击这里

An alternative to AMagyar's answer. AMagyar 答案的替代方案。 This version works when you click on element that gets removed from the DOM with an ngIf.当您单击使用 ngIf 从 DOM 中删除的元素时,此版本有效。

http://plnkr.co/edit/4mrn4GjM95uvSbQtxrAS?p=preview 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; }

Binding to document click through @Hostlistener is costly.通过@Hostlistener 绑定到文档点击成本很高。 It can and will have a visible performance impact if you overuse(for example, when building a custom dropdown component and you have multiple instances created in a form).如果您过度使用(例如,在构建自定义下拉组件并且您在表单中创建了多个实例时),它可以并且将会产生明显的性能影响。

I suggest adding a @Hostlistener() to the document click event only once inside your main app component.我建议只在您的主应用程序组件内向文档单击事件添加一次@Hostlistener()。 The event should push the value of the clicked target element inside a public subject stored in a global utility service.该事件应该将单击的目标元素的值推送到存储在全局实用程序服务中的公共主题中。

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

Whoever is interested for the clicked target element should subscribe to the public subject of our utilities service and unsubscribe when the component is destroyed.任何对点击的目标元素感兴趣的人都应该订阅我们实用程序服务的公共主题,并在组件被销毁时取消订阅。

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
  }

Improving @J.改进@J。 Frankenstein answear科学怪人回答

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

Above mentioned answers are correct but what if you are doing a heavy process after losing the focus from the relevant component.上面提到的答案是正确的,但是如果您在失去相关组件的焦点后正在做一个繁重的过程怎么办。 For that, I came with a solution with two flags where the focus out event process will only take place when losing the focus from relevant component only.为此,我提出了一个带有两个标志的解决方案,其中只有在失去相关组件的焦点时才会发生焦点事件过程。

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

Hope this will help you.希望这会帮助你。 Correct me If have missed anything.纠正我如果错过了什么。

ginalx's answer should be set as the default one imo: this method allows for many optimizations. ginalx 的答案应该设置为默认的 imo:此方法允许进行许多优化。

The problem问题

Say that we have a list of items and on every item we want to include a menu that needs to be toggled.假设我们有一个项目列表,并且我们希望在每个项目上包含一个需要切换的菜单。 We include a toggle on a button that listens for a click event on itself (click)="toggle()" , but we also want to toggle the menu whenever the user clicks outside of it.我们在一个按钮上添加了一个切换按钮,该按钮侦听自身的click事件(click)="toggle()" ,但我们也希望在用户点击菜单之外时切换菜单。 If the list of items grows and we attach a @HostListener('document:click') on every menu, then every menu loaded within the item will start listening for the click on the entire document, even when the menu is toggled off.如果项目列表增长并且我们在每个菜单上附加一个@HostListener('document:click') ,那么在项目中加载的每个菜单都将开始监听整个文档的点击,即使菜单被关闭。 Besides the obvious performance issues, this is unnecessary.除了明显的性能问题外,这是不必要的。

You can, for example, subscribe whenever the popup gets toggled via a click and start listening for "outside clicks" only then.例如,您可以在弹出窗口通过点击切换时订阅,然后才开始监听“外部点击”。


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

And, in *.component.html :并且,在*.component.html中:


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

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

Alternative to MVP, you only need to watch for Event MVP的替代方案,只需要关注Event

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

Solution解决方案

Get all parents让所有的父母

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

Checks if any parent is the component检查是否有任何父组件是组件

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

If you have the component as parent then click inside the component如果您将组件作为父组件,请在组件内部单击

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

Complete method完整方法

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

u can call event function like (focusout) or (blur) then u put your code你可以调用像(focusout)或(blur)这样的事件函数然后你把你的代码

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

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

another possible solution using event.stopPropagation()使用event.stopPropagation()的另一种可能的解决方案

  1. define a click listener on top most parent component which clears the click-inside variable在最顶层的父组件上定义一个点击监听器,它会清除 click-inside 变量
  2. define a click listener on the child component which first calls the event.stopPropagation() and then sets the click-inside variable在子组件上定义一个点击监听器,它首先调用 event.stopPropagation() 然后设置 click-inside 变量
import { Directive, ElementRef, HostListener, Output } from '@angular/core';
import { Subject } from 'rxjs';
import { filter, tap, withLatestFrom } from 'rxjs/operators';

@Directive({
  selector: '[clickOutside]',
})
export class ClickOutsideDirective {
  @Output() clickOutside = new Subject<boolean>();
  mouseDownOutside$ = new Subject<boolean>();
  mouseUpOutside$ = new Subject<boolean>();
  clickOutsideSub;
  constructor(private elementRef: ElementRef) {
    this.clickOutsideSub = this.mouseUpOutside$
      .pipe(
        withLatestFrom(this.mouseDownOutside$),
        filter(([mouseUpOutside, mouseDownOutside]) => {
          return mouseDownOutside && mouseUpOutside;
        }),
        tap(() => {
          this.clickOutside.next(true);
        })
      )
      .subscribe();
  }

  @HostListener('window:mousedown', ['$event'])
  mousedown(event) {
    this.elementRef.nativeElement.contains(event.target)
      ? this.mouseDownOutside$.next(false)
      : this.mouseDownOutside$.next(true);
  }

  @HostListener('window:mouseup', ['$event'])
  mouseup(event) {
    this.elementRef.nativeElement.contains(event.target)
      ? this.mouseUpOutside$.next(false)
      : this.mouseUpOutside$.next(true);
  }

  ngOnDestroy() {
    this.clickOutsideSub?.unsubscribe();
  }
}

Then you can use that directive like below:然后您可以使用该指令,如下所示:

  <div (clickOutside)="closeModal()" class="modal">
     {{ modalInfo }}
  <button (click)="closeModal()">Close</button>
  </div>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM