如何关闭外部点击的下拉菜单?

Cle*_*ent 129 javascript rxjs drop-down-menu angular

当用户点击该下拉菜单之外的任何地方时,我想关闭我的登录菜单下拉菜单,我想用Angular2和Angular2"方法"来做...

我已经实施了一个解决方案,但我真的对它没有信心.我认为必须有一种最简单的方法来实现相同的结果,所以如果你有任何想法......让我们讨论:)!

这是我的实现:

下拉组件:

这是我的下拉列表的组件:

  • 每次将此组件设置为可见时(例如:当用户单击按钮以显示它时),它会订阅存储在SubjectsService中的"全局"rxjs主题userMenu.
  • 每次隐藏时,它都会取消订阅此主题.
  • 该组件的模板中的任何位置的每次单击都会触发onClick()方法,该方法只是将事件冒泡停止到顶部(和应用程序组件)

这是代码

export class UserMenuComponent {

    _isVisible: boolean = false;
    _subscriptions: Subscription<any> = null;

    constructor(public subjects: SubjectsService) {
    }

    onClick(event) {
        event.stopPropagation();
    }

    set isVisible(v) {
        if( v ){
            setTimeout( () => {
this._subscriptions =  this.subjects.userMenu.subscribe((e) => {
                       this.isVisible = false;
                       })
            }, 0);
        } else {
            this._subscriptions.unsubscribe();
        }
        this._isVisible = v;
    }

    get isVisible() {
        return this._isVisible;
    }
}
Run Code Online (Sandbox Code Playgroud)

应用程序组件:

另一方面,有应用程序组件(它是下拉组件的父级):

  • 此组件捕获每个单击事件并在相同的rxjs上发出主题(userMenu)

这是代码:

export class AppComponent {

    constructor( public subjects: SubjectsService) {
        document.addEventListener('click', () => this.onClick());
    }
    onClick( ) {
        this.subjects.userMenu.next({});
    }
}
Run Code Online (Sandbox Code Playgroud)

什么打扰我:

  1. 我觉得让一个全局主题充当这些组件之间的连接器的想法让我感到很自在.
  2. setTimeout的:这是必要的,因为这里是,如果在此按钮,用户点击,显示的下拉什么,否则会发生:
    • 用户单击按钮(不是下拉组件的一部分)以显示下拉列表.
    • 将显示下拉列表,并立即订阅userMenu主题.
    • 点击事件会冒泡到应用程序组件并被捕获
    • 应用程序组件在userMenu主题上发出事件
    • 下拉组件在userMenu上捕获此操作并隐藏下拉列表.
    • 最后,永远不会显示下拉列表.

这设置超时延迟订阅到当前JavaScript代码的结束转而解决问题,但在我看来以非常优雅的方式.

如果您了解更清洁,更好,更智能,更快速或更强大的解决方案,请告诉我:)!

Sas*_*sxa 225

你可以使用(document:click)事件:

@Component({
  host: {
    '(document:click)': 'onClick($event)',
  },
})
class SomeComponent() {
  constructor(private _eref: ElementRef) { }

  onClick(event) {
   if (!this._eref.nativeElement.contains(event.target)) // or some similar check
     doSomething();
  }
}
Run Code Online (Sandbox Code Playgroud)

另一种方法是将自定义事件创建为指令.看看Ben Nadel的这些帖子:

  • 根据官方的Angular 2样式指南,您应该在`Component`装饰器上使用`@HostListener('document:click',['$ event'])`而不是`host`属性. (35认同)
  • 或者你可以只使用rxjs,比如`Observable.fromEvent(document,'click').subscribe(event => {your code here})`,所以你总是可以只在你需要听的时候订阅,比如你打开了下拉菜单,当你关闭它时,你取消订阅 (13认同)
  • 这种技术的唯一缺点是,现在您的应用程序中有一个click事件侦听器,每次单击时都会触发. (8认同)
  • 如果event.target是通过[innerHTML]绑定之类的东西动态添加的元素,那么elementRef的nativeElement将不包含它. (4认同)

Kam*_*ski 34

优雅的方法

我找到了这个clickOut指令:https: //github.com/chliebel/angular2-click-outside.我检查它,它运作良好(我只复制clickOutside.directive.ts到我的项目).你可以这样使用它:

<div (clickOutside)="close($event)"></div>
Run Code Online (Sandbox Code Playgroud)

close当用户在div外部单击时,您的函数将在何处调用.处理问题所描述的问题是非常优雅的方式.

如果你使用above指令关闭popUp窗口,请记住首先添加event.stopPropagation()到打开popUp的按钮单击事件处理程序.

奖金:

下面我从文件中复制oryginal指令代码clickOutside.directive.ts(如果链接将来会停止工作) - 作者是Christian Liebel:

import {Directive, ElementRef, Output, EventEmitter, HostListener} from '@angular/core';
    
@Directive({
    selector: '[clickOutside]'
})
export class ClickOutsideDirective {
    constructor(private _elementRef: ElementRef) {
    }

    @Output()
    public clickOutside = new EventEmitter<MouseEvent>();

    @HostListener('document:click', ['$event', '$event.target'])
    public onClick(event: MouseEvent, targetElement: HTMLElement): void {
        if (!targetElement) {
            return;
        }

        const clickedInside = this._elementRef.nativeElement.contains(targetElement);
        if (!clickedInside) {
            this.clickOutside.emit(event);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @Vega我的推荐是在带有*ngIf的元素中使用Directive,在下拉列表的情况下,这可能类似于`<div class ="wrap"*ngIf ="isOpened"(clickOutside)="... //这应该设置this.isOpen = false"` (2认同)

Ton*_*ony 18

我这样做了.

在文档中添加了一个事件监听器,click并在该处理程序中检查了我是否container包含event.target,如果不包含- 隐藏下拉列表.

它看起来像这样.

@Component({})
class SomeComponent {
    @ViewChild('container') container;
    @ViewChild('dropdown') dropdown;

    constructor() {
        document.addEventListener('click', this.offClickHandler.bind(this)); // bind on doc
    }

    offClickHandler(event:any) {
        if (!this.container.nativeElement.contains(event.target)) { // check click origin
            this.dropdown.nativeElement.style.display = "none";
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


JuH*_*m89 16

我认为Sasxa接受了大多数人的回答.但是,我遇到了一种情况,即应该监听off-click事件的Element内容会动态更改.所以Elements nativeElement在动态创建时不包含event.target.我可以用以下指令解决这个问题

@Directive({
  selector: '[myOffClick]'
})
export class MyOffClickDirective {

  @Output() offClick = new EventEmitter();

  constructor(private _elementRef: ElementRef) {
  }

  @HostListener('document:click', ['$event.path'])
  public onGlobalClick(targetElementPath: Array<any>) {
    let elementRefInPath = targetElementPath.find(e => e === this._elementRef.nativeElement);
    if (!elementRefInPath) {
      this.offClick.emit(null);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

我检查elementRef是否在事件的路径(DOM目标路径)中,而不是检查elementRef是否包含event.target.这样就可以处理动态创建的元素.


Xav*_*ier 11

如果您在iOS上执行此操作,请同时使用该touchstart事件:

从Angular 4开始,HostListener装饰是执行此操作的首选方式

import { Component, OnInit, HostListener, ElementRef } from '@angular/core';
...
@Component({...})
export class MyComponent implement OnInit {

  constructor(private eRef: ElementRef){}

  @HostListener('document:click', ['$event'])
  @HostListener('document:touchstart', ['$event'])
  handleOutsideClick(event) {
    // Some kind of logic to exclude clicks in Component.
    // This example is borrowed Kamil's answer
    if (!this.eRef.nativeElement.contains(event.target) {
      doSomethingCool();
    }
  }

}
Run Code Online (Sandbox Code Playgroud)


小智 10

我们今天一直在研究一个类似的问题,试图找出如何让dropdown div在被点击时消失.我们与初始海报的问题略有不同,因为我们不想点击其他组件指令,而只是在特定div之外.

我们最终通过使用(window:mouseup)事件处理程序来解决它.

步骤:
1.)我们给整个下拉菜单div一个唯一的类名.

2.)在内部下拉菜单本身(我们想要点击不关闭菜单的唯一部分),我们添加了一个(window:mouseup)事件处理程序并传入$ event.

注意:使用典型的"单击"处理程序无法完成,因为这与父单击处理程序冲突.

3.)在我们的控制器中,我们创建了我们想要在click out事件中调用的方法,并且我们使用event.closest(这里是docs)来查明点击的点是否在我们的目标类div中.

 autoCloseForDropdownCars(event) {
        var target = event.target;
        if (!target.closest(".DropdownCars")) { 
            // do whatever you want here
        }
    }
Run Code Online (Sandbox Code Playgroud)
 <div class="DropdownCars">
   <span (click)="toggleDropdown(dropdownTypes.Cars)" class="searchBarPlaceholder">Cars</span>
   <div class="criteriaDropdown" (window:mouseup)="autoCloseForDropdownCars($event)" *ngIf="isDropdownShown(dropdownTypes.Cars)">
   </div>
</div>
Run Code Online (Sandbox Code Playgroud)

  • 只要密切注意浏览器的兼容性,IE/Edge就不支持`.closest()`([caniuse](http://caniuse.com/#feat=element-closest)) (2认同)

Ton*_*Ngo 8

mouseleave您可以像这样在您的视图中使用

用 Angular 8 测试并完美工作

<ul (mouseleave)="closeDropdown()"> </ul>
Run Code Online (Sandbox Code Playgroud)


Pat*_*ham 5

您可以为覆盖整个屏幕的下拉列表创建一个兄弟元素,该元素将不可见并且仅用于捕获点击事件。然后您可以检测对该元素的点击并在点击时关闭下拉列表。让我们说元素是丝印类,这是它的一些样式:

.silkscreen {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1;
}
Run Code Online (Sandbox Code Playgroud)

z-index 需要足够高以将其定位在除下拉列表之外的所有内容之上。在这种情况下,我的下拉列表将 b z-index 2。

其他答案在某些情况下对我有用,除了有时当我与其中的元素交互时我的下拉菜单关闭,我不想要那样。我根据事件目标动态添加了未包含在我的组件中的元素,就像我预期的那样。我想我不会用丝网印刷的方式来解决这个问题。


小智 5

我没有任何解决方法。我刚刚附加了文档:单击切换功能,如下所示:

    @指示({
      选择器:“ [appDropDown]”
    })
    导出类DropdownDirective实现OnInit {

      @HostBinding('class.open')isOpen:布尔值;

      构造函数(私有elemRef:ElementRef){}

      ngOnInit():void {
        this.isOpen = false;
      }

      @HostListener('document:click',['$ event'])
      @HostListener('document:touchstart',['$ event'])
      切换(事件){
        如果(this.elemRef.nativeElement.contains(event.target)){
          this.isOpen =!this.isOpen;
        }其他{
          this.isOpen = false;
      }
    }

因此,当我超出指令范围时,我将关闭下拉列表。