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

IT技术 javascript drop-down-menu angular rxjs
2021-01-23 17:19:20

当用户点击该下拉菜单之外的任何地方时,我想关闭我的登录菜单下拉菜单,我想用 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;
    }
}

应用组件:

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

  • 该组件捕获每个点击事件并在同一个 rxjs 主题(userMenu上发出

这是代码:

export class AppComponent {

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

什么困扰我:

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

这个设置超时将订阅延迟到当前 JavaScript 代码轮的末尾,这解决了问题,但在我看来以一种非常优雅的方式。

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

6个回答

您可以使用(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();
  }
}

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

这种技术的唯一缺点是现在您的应用程序中有一个单击事件侦听器,每次单击时都会触发该侦听器。
2021-03-16 17:19:20
@Sasxa 谢谢,并同意。我想如果有一个非弃用的 API 文档,它会出现在搜索中,引导我来到这里。
2021-03-25 17:19:20
如果 event.target 是通过诸如 [innerHTML] 绑定之类的东西动态添加的元素,则 elementRef 的 nativeElement 将不包含它。
2021-03-28 17:19:20
根据官方的 Angular 2 风格指南,您应该装饰器使用@HostListener('document:click', ['$event'])而不是host属性Component
2021-04-07 17:19:20
或者您可以只使用 rxjs,例如Observable.fromEvent(document, 'click').subscribe(event => {your code here}),这样您就可以始终仅在需要收听时订阅,例如您打开下拉列表,当您关闭它时您取消订阅
2021-04-08 17:19:20

优雅的方法

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

<div (clickOutside)="close($event)"></div>

close当用户在 div 外单击时将调用的函数在哪里这是处理问题中描述的问题的非常优雅的方式。

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

奖金:

下面我从文件中复制原始指令代码clickOutside.directive.ts(以防链接将来停止工作) - 作者是Christian Liebel

@Vega 我的建议是在带有 *ngIf 的元素中使用指令,在下拉列表的情况下,这可能类似于 <div class="wrap" *ngIf="isOpened" (clickOutside)="...// this should set this.isOpen=false"
2021-04-01 17:19:20

我是这样做的。

在文档上添加了一个事件侦听器,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";
        }
    }
}
你好。.bind(this) 是必要的吗?
2021-04-03 17:19:20
@Brian 这可能是必要的,也可能不是,但如果他将其包装this.offClickHandler在箭头函数中,肯定不会
2021-04-05 17:19:20

我认为 Sasxa 接受的答案适用于大多数人。但是,我遇到了一种情况,即应该侦听关闭点击事件的 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);
    }
  }
}

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

这对我很有帮助。不知道为什么其他答案没有检测到组件之外的点击。
2021-03-30 17:19:20
谢谢 - 当存在子组件时,这会更好
2021-04-12 17:19:20

如果您在 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();
    }
  }

}