简体   繁体   English

如何检测Angular中元素外部的点击?

[英]How to detect click outside of an element in Angular?

This div will be dynamically shown on the page as an east panel when the open panel button is clicked.单击open panel按钮时,此div将作为东面板动态显示在页面上。 The bool showEastPanel variable is what is used to open and close the east panel. bool showEastPanel变量用于打开和关闭东面板。 I am trying to use (clickoutside) to close the panel (setting showEastPanel to false) however the open panel runs first on the Angular hook and the panel is set to true then false and the panel doesn't display.我正在尝试使用(clickoutside)关闭面板(将showEastPanel设置为 false),但是打开的面板首先在Angular挂钩上运行,并且面板设置为 true 然后 false 并且面板不显示。 Is the any way to scope clickoutside to not include the button?是否有任何方法可以将clickoutside范围clickoutside为不包含按钮?

 <div [ngClass]="{'d-none': !showEastPanel, 'east-panel-container': showEastPanel}" (clickOutside)="ClosePanel()"> <div id="east-panel"> <ng-template #eastPanel></ng-template> </div> </div> <button (click)="ShowPanel()">Open Panel</button>

Here is the link to the working demo: Stackblitz Demo这是工作演示的链接: Stackblitz Demo

I would do this by using the Angular recommended approach which is also easy to develop apps in environments with no DOM access, I mean Renderer 2 class which is an abstraction provided by Angular in the form of a service that allows manipulating elements of your app without having to touch the DOM directly.我会通过使用 Angular 推荐的方法来做到这一点,该方法也很容易在没有 DOM 访问的环境中开发应用程序,我的意思是Renderer 2类,它是 Angular 以服务形式提供的抽象,允许操作应用程序的元素而无需必须直接接触 DOM。

In this approach, you need to inject Renderer2 into your component constructor, which the Renderer2 lets us to listen to triggered events elegantly.在这种方法中,您需要将Renderer2注入到您的组件构造函数中, Renderer2可以让我们优雅地listen触发事件。 It just takes the element you're going to listen on as the first argument which can be window , document , body or any other element reference.它只是将您要监听的元素作为第一个参数,它可以是windowdocumentbody或任何其他元素引用。 For the second argument it takes the event we're going to listen on which in this case is click , and the third argument is actually the callback function which we do it by arrow function.对于第二个参数,它接受我们要监听的事件,在这种情况下是click ,第三个参数实际上是我们通过箭头函数执行的回调函数。

this.renderer.listen('window', 'click',(e:Event)=>{ // your code here})

The rest of the solution is easy, you just need to set a boolean flag which keeps the status of the menu (or panel) visibility, and what we should do is to assign false to that flag when it's clicked outside of the menu.解决方案的其余部分很简单,您只需要设置一个布尔标志来保持菜单(或面板)可见性的状态,我们应该做的是在菜单外单击时为该标志分配false

HTML HTML

<button #toggleButton (click)="toggleMenu()"> Toggle Menu</button>

<div class="menu" *ngIf="isMenuOpen" #menu>
I'm the menu. Click outside to close me
</div>

app.component.ts app.component.ts

    export class AppComponent {
      /**
       * This is the toogle button elemenbt, look at HTML and see its defination
       */
      @ViewChild('toggleButton') toggleButton: ElementRef;
      @ViewChild('menu') menu: ElementRef;
    
      constructor(private renderer: Renderer2) {
        /**
         * This events get called by all clicks on the page
         */
        this.renderer.listen('window', 'click',(e:Event)=>{
             /**
              * Only run when toggleButton is not clicked
              * If we don't check this, all clicks (even on the toggle button) gets into this
              * section which in the result we might never see the menu open!
              * And the menu itself is checked here, and it's where we check just outside of
              * the menu and button the condition abbove must close the menu
              */
            if(e.target !== this.toggleButton.nativeElement && e.target!==this.menu.nativeElement){
                this.isMenuOpen=false;
            }
        });
      }
    
      isMenuOpen = false;
    
      toggleMenu() {
        this.isMenuOpen = !this.isMenuOpen;
      }
    }

Again, if you like to see the working demo, use this link: Stackblitz Demo同样,如果您想查看工作演示,请使用此链接: Stackblitz Demo

you can do something like this你可以做这样的事情

  @HostListener('document:mousedown', ['$event'])
  onGlobalClick(event): void {
     if (!this.elementRef.nativeElement.contains(event.target)) {
        // clicked outside => close dropdown list
     this.isOpen = false;
     }
  }

and use *ngIf=isOpen for the panel并为面板使用 *ngIf=isOpen

I would like to add the solution that helped me to achieve proper result.我想添加帮助我获得正确结果的解决方案。

When using embedded elements and you want to detect click on parent, event.target gives reference to the basic child.当使用嵌入元素并且您想要检测父级上的点击时, event.target 提供对基本子级的引用。

HTML HTML

<div #toggleButton (click)="toggleMenu()">
    <u>Toggle Menu</u>
    <span class="some-icon"></span>
</div>

<div #menu class="menu" *ngIf="isMenuOpen">
    <h1>I'm the menu.</h1>
    <div>
        I have some complex content containing multiple children.
        <i>Click outside to close me</i>
    </div>
</div>

I click on "Toggle menu" text, event.target returns reference to 'u' element instead of #toggleButton div.我单击“切换菜单”文本,event.target 返回对'u'元素而不是#toggleButton div 的引用

For this case I used M98's solution including Renderer2, but changed the condition to the one from Sujay's answer.对于这种情况,我使用了 M98 的解决方案,包括 Renderer2,但将条件更改为 Sujay 的回答中的条件。

ToggleButton.nativeElement.contains(e.target) returns true even if the target of click event is in nativeElement's children, which solves the problem. ToggleButton.nativeElement.contains(e.target) 即使单击事件的目标在 nativeElement 的子级中也返回true ,从而解决了问题。

component.ts组件.ts

export class AppComponent {
/**
 * This is the toogle button element, look at HTML and see its definition
 */
    @ViewChild('toggleButton') toggleButton: ElementRef;
    @ViewChild('menu') menu: ElementRef;
    isMenuOpen = false;

    constructor(private renderer: Renderer2) {
    /**
     * This events get called by all clicks on the page
     */
        this.renderer.listen('window', 'click',(e:Event)=>{
            /**
             * Only run when toggleButton is not clicked
             * If we don't check this, all clicks (even on the toggle button) gets into this
             * section which in the result we might never see the menu open!
             * And the menu itself is checked here, and it's where we check just outside of
             * the menu and button the condition abbove must close the menu
             */
            if(!this.toggleButton.nativeElement.contains(e.target) && !this.menu.nativeElement.contains(e.target)) {
                this.isMenuOpen=false;
            }
        });
    }

    toggleMenu() {
        this.isMenuOpen = !this.isMenuOpen;
    }
}

Here's a reusable directive, it also cover the case if the element is inside an ngIf:这是一个可重用的指令,它也涵盖了元素在 ngIf 内的情况:

import { Directive, ElementRef, Optional, Inject, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { fromEvent, Subscription } from 'rxjs';
import { DOCUMENT } from '@angular/common';
import { filter } from 'rxjs/operators';

@Directive({
  selector: '[outsideClick]',
})
export class OutsideClickDirective implements OnInit, OnDestroy {
  @Output('outsideClick') outsideClick = new EventEmitter<MouseEvent>();

  private subscription: Subscription;

  constructor(private element: ElementRef, @Optional() @Inject(DOCUMENT) private document: any) {}

  ngOnInit() {
    setTimeout(() => {
      this.subscription = fromEvent<MouseEvent>(this.document, 'click')
        .pipe(
          filter(event => {
            const clickTarget = event.target as HTMLElement;
            return !this.isOrContainsClickTarget(this.element.nativeElement, clickTarget);
          }),
        )
        .subscribe(event => this.outsideClick.emit());
    }, 0);
  }

  private isOrContainsClickTarget(element: HTMLElement, clickTarget: HTMLElement) {
    return element == clickTarget || element.contains(clickTarget);
  }

  ngOnDestroy() {
    if (this.subscription) this.subscription.unsubscribe();
  }
}

Credits to https://github.com/ngez/platform , I got most of the logic out of it.归功于https://github.com/ngez/platform ,我从中获得了大部分逻辑。

What I was missing was the setTimeout(..., 0), which makes sure to schedule the check after the component using the directive has been rendered.我缺少的是 setTimeout(..., 0),它确保在使用指令的组件呈现后安排检查。

Useful links:有用的链接:

I like the answer by Sujay.我喜欢 Sujay 的回答。 If you wish to create a directive instead (to be used in several components).如果您希望创建一个指令(在多个组件中使用)。 This is how I would do it.这就是我要做的。

import {
  Directive,
  EventEmitter,
  HostListener,
  Output,
  ElementRef,
} from '@angular/core';

@Directive({
  selector: '[outsideClick]',
})
export class OutsideClickDirective {
  @Output()
  outsideClick: EventEmitter<MouseEvent> = new EventEmitter();

  @HostListener('document:mousedown', ['$event'])
  onClick(event: MouseEvent): void {
    if (!this.elementRef.nativeElement.contains(event.target)) {
      this.outsideClick.emit(event);
    }
  }

  constructor(private elementRef: ElementRef) {}
}

You would then use the directive like so:然后你可以像这样使用指令:

<div class="menu" *ngIf="isMenuOpen" (outsideClick)="isMenuOpen = false" outsideClick #menu>
  I'm the menu. Click outside to close me
</div>

i did other way unlike the previous answers.与以前的答案不同,我做了其他方式。

i put mouseleave , mouseenter event on dropdown menu我把mouseleave , mouseenter事件放在下拉菜单上

<div
    class="dropdown-filter"
    (mouseleave)="onMouseOutFilter($event)"
    (mouseenter)="onMouseEnterFilter($event)"
  >
    <ng-container *ngIf="dropdownVisible">
      <input
        type="text"
        placeholder="search.."
        class="form-control"
        [(ngModel)]="keyword"
        id="myInput"
        (keyup)="onKeyUp($event)"
      />
    </ng-container>
    <ul
      class="dropdown-content"
      *ngIf="dropdownVisible"
    >
      <ng-container *ngFor="let item of filteredItems; let i = index">
        <li
          (click)="onClickItem($event, item)"
          [ngStyle]="listWidth && {width: listWidth + 'px'}"
        >
          <span>{{ item.label }}</span>
        </li>
      </ng-container>
    </ul>
  </div>
  constructor(private renderer: Renderer2) {
    /**
     * this.renderer instance would be shared with the other multiple same components
     * so you should have one more flag to divide the components
     * the only dropdown with mouseInFilter which is false should be close
     */
    this.renderer.listen('document', 'click', (e: Event) => {
      if (!this.mouseInFilter) {
        // this is the time to hide dropdownVisible
        this.dropdownVisible = false;
      }
    });
  }

  onMouseOutFilter(e) {
    this.mouseInFilter = false;
  }

  onMouseEnterFilter(e) {
    this.mouseInFilter = true;
  }

and make sure the defaultValue of mouseInFilter is false;并确保 mouseInFilter 的 defaultValue 为 false;

  ngOnInit() {
    this.mouseInFilter = false;
    this.dropdownVisible = false;
  }

and when dropdown should be visible mouseInFilter is going to be true当下拉菜单应该可见时,mouseInFilter 将是真的

  toggleDropDownVisible() {
    if (!this.dropdownVisible) {
      this.mouseInFilter = true;
    }
    this.dropdownVisible = !this.dropdownVisible;
  }

I have did the same in one of my requirement to show mega menu popup when user clicks on menu icon but want to close it whet user clicks outside it.我在我的一项要求中做了同样的事情,当用户点击菜单图标时显示超级菜单弹出窗口,但想要关闭它,用户点击它之外。 Here i am trying to prevent click on icon as well.在这里,我也试图防止点击图标。 Please have a look.请看一看。

In HTML在 HTML 中

 <div #menuIcon (click)="onMenuClick()">
  <a><i class="fa fa-reorder"></i></a>
 </div>
<div #menuPopup  *ngIf="showContainer">
   <!-- Something in the popup like menu -->
</div>

In TS在 TS

  @ViewChild('menuIcon', { read: ElementRef, static: false })  menuIcon: ElementRef;
  @ViewChild('menuPopup', { read: ElementRef, static: false })  menuPopup: ElementRef;
   showContainer = false;

      constructor(private renderer2: Renderer2) {
      this.renderer2.listen('window', 'click', (e: Event) => {
        if (
         (this.menuPopup && this.menuPopup.nativeElement.contains(e.target)) ||
          (this.menuIcon && this.menuIcon.nativeElement.contains(e.target))
         ) {
              // Clicked inside plus preventing click on icon
             this.showContainer = true;
           } else {
             // Clicked outside
             this.showContainer = false;
         }
      });
    }

     onMenuClick() {
        this.isShowMegaMenu = true;
      }

You can use https://github.com/arkon/ng-click-outside which is pretty easy to use with a lot of useful features:您可以使用https://github.com/arkon/ng-click-outside ,它非常易于使用,具有许多有用的功能:

@Component({
  selector: 'app',
  template: `
    <div (clickOutside)="onClickedOutside($event)">Click outside this</div>
  `
})
export class AppComponent {
  onClickedOutside(e: Event) {
    console.log('Clicked outside:', e);
  }
}

About performance , the lib uses ngOnDestroy to remove the listener when the directive isn't active (use clickOutsideEnabled property) which is really important and most of proposed solutions don't do that.关于性能,当指令未激活时,lib 使用ngOnDestroy删除侦听器(使用clickOutsideEnabled属性),这非常重要,并且大多数提议的解决方案都没有这样做。 See the source code here .请参阅此处的源代码。

thanks Emerica ng-click-outside works perfect, this is what i was needing, i was testing on my modal, but when i click it, the first click on the button, it dettects outside click and then didnt work to put on modal, but i only added delayClickOutsideInit="true" from docs and it works very good, this is the final result:感谢 Emerica ng-click-outside工作完美,这就是我所需要的,我正在测试我的模态,但是当我点击它时,第一次点击按钮,它检测到外部点击,然后没有工作来放置模态,但我只从文档中添加了delayClickOutsideInit="true"并且效果很好,这是最终结果:

<button
  (click)="imageModal()"
>
<button/>

<div
  *ngIf="isMenuOpen"
>
  <div
    (clickOutside)="onClickedOutside($event)"
    delayClickOutsideInit="true"
  >
   Modal content
  </div>
</div>

and this is my component这是我的组件

import {
  Component,
} from '@angular/core';

@Component({
  selector: 'app-modal-header',
  templateUrl: './modal-header.component.html',
  styleUrls: ['./modal-header.component.css'],
})
export class ModalHeaderComponent implements OnInit {
  public isMenuOpen = false;

  constructor() {}

  imageModal() {
    this.isMenuOpen = !this.isMenuOpen;
  }
  closeModal() {
//you can do an only close function click
    this.isMenuOpen = false;
  }
  onClickedOutside(e: Event) {
    this.isMenuOpen = false;
  }
}

More Simplified Code with demo on: StackBlitz带有演示的更简化代码: StackBlitz

I have made a common function to close the menu on outside click and Prevent the closeing if click triggered on specific elements.我做了一个通用功能来关闭外部点击菜单,并防止在特定元素上触发点击时关闭。

HTML HTML

<button (click)="toggleMenu(); preventCloseOnClick()">Toggle Menu</button>
<ul (click)="preventCloseOnClick()" *ngIf="menuOpen">
  <li>Menu 1</li>
  <li>Menu 2</li>
  <li>Menu 3</li>
  <li>Menu 4</li>
  <li>Menu 5</li>
</ul>

TS TS

import { Component, VERSION, Renderer2 } from '@angular/core';

export class AppComponent {
  menuOpen: boolean = false;
  menuBtnClick: boolean = false;

  constructor(private renderer: Renderer2) {
    this.renderer.listen('window', 'click', (e: Event) => {
      if (!this.menuBtnClick) {
        this.menuOpen = false;
      }
      this.menuBtnClick = false;
    });
  }
  toggleMenu() {
    this.menuOpen = !this.menuOpen;
  }
  preventCloseOnClick() {
    this.menuBtnClick = true;
  }
}

if the click() and clickOutside() are getting triggered simultaneously then you must refer如果 click() 和 clickOutside() 同时被触发,那么你必须参考

https://github.com/arkon/ng-click-outside/issues/31 https://github.com/arkon/ng-click-outside/issues/31

it fixed my issue它解决了我的问题

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

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