如何closures外部点击下拉菜单?

我想closures我的login菜单下拉菜单,当用户点击下拉菜单中的任何地方,我想用Angular2和Angular2的“方法”来做到这一点…

我已经实施了一个解决scheme,但我真的不觉得有信心。 我认为必须有一个最简单的方法来达到同样的结果,所以如果你有任何想法…让我们来讨论:)!

这是我的实现:

下拉组件:

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

  • 每次将此组件设置为可见时(例如:当用户单击某个button以显示该组件时),它将订阅存储在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 :这是需要的,因为如果用户点击显示下拉列表的button,则会发生以下情况:
    • 用户点击button(不是下拉组件的一部分)显示下拉菜单。
    • 显示下拉菜单,立即订阅userMenu主题
    • 点击事件冒泡到应用程序组件并被捕获
    • 应用程序组件在userMenu主题上发出一个事件
    • 下拉组件在userMenu上捕获此操作并隐藏下拉列表。
    • 最后,下拉菜单从不显示。

这个设置的超时时间延迟了订阅到当前JavaScript代码的结束,从而解决了问题,但是在我看来,这是非常优雅的方式。

如果你知道更清洁,更好,更聪明,更快或更强的解决scheme,请让我知道:)!

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

另一种方法是创build自定义事件作为指令。 看看这些postBen Nadel:

  • 跟踪单击事件-外的电stream分量
  • select-和输出,可以具备最同名
  • DirectiveMetadata
  • 主机绑定

我这样做了。

添加一个文档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"; } } } 

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

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

脚步:
1.)我们给整个下拉菜单div一个独特的类名称。

2.)在内部下拉菜单本身(我们希望单击的不是closures菜单的唯一部分),我们添加了一个(window:mouseup)事件处理函数,并在$事件中传递。

注:它不能用一个典型的“点击”处理程序,因为这与父点击处理程序冲突。

3.)在我们的控制器中,我们创build了我们想要在点击事件中调用的方法,并且使用event.closest( docs here )来确定点击的点是否在我们的目标类div中。

  autoCloseForDropdownCars(event) { var target = event.target; if (!target.closest(".DropdownCars")) { // do whatever you want here } } 
  <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> 

我发现这个clickOut指令: https : //github.com/chliebel/angular2-click-outside

我检查它,它运作良好(我只复制clickOutside.directive.ts到我的项目)。 你可以这样使用它:

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

在哪里close是你的function,这将是在用户点击外部div时调用。 处理问题中描述的问题是非常优雅的方式。

===指令码===

下面我从文件clickOutside.directive.ts复制oryginal指令代码(如果链接将来会停止工作) – 作者是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); } } } 

我认为Sasxa接受了大多数人的答案。 不过,我有一种情况,应该监听脱机事件的元素的内容dynamic变化。 因此元素nativeElement在dynamic创build时不包含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是否在事件的path(目标的DOMpath)。 这样就可以处理dynamic创build的元素。

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

您可以在下拉菜单中创build一个同级元素,该元素将覆盖整个不可见的屏幕,并仅用于捕获点击事件。 然后,您可以检测该元素上的点击,并在点击时closures下拉菜单。 可以说元素是丝绸类的,下面是它的一些风格:

 .silkscreen { position: fixed; top: 0; bottom: 0; left: 0; right: 0; z-index: 1; } 

z-index需要足够高,以便将其放置在所有位置上,而不是放在下拉菜单中。 在这种情况下,我的下拉菜单是z-index 2。

其他答案在某些情况下适用于我,除了有时我的下拉菜单closures,当我与它内的元素进行交互,我不想这样做。 根据活动目标,我已经dynamic地添加了组件中未包含的元素,就像我期望的那样。 我觉得我只是尝试一下丝网印刷的方式,而不是把那些乱七八糟的东西整理一下。

 import { Component, HostListener } from '@angular/core'; @Component({ selector: 'custom-dropdown', template: ` <div class="custom-dropdown-container"> Dropdown code here </div> ` }) export class CustomDropdownComponent { thisElementClicked: boolean = false; constructor() { } @HostListener('click', ['$event']) onLocalClick(event: Event) { this.thisElementClicked = true; } @HostListener('document:click', ['$event']) onClick(event: Event) { if (!this.thisElementClicked) { //click was outside the element, do stuff } this.thisElementClicked = false; } } 

DOWNSIDES: – 在页面上为这些组件中的每一个组件单击两个事件监听器。 不要在页面上的组件上使用这个数百次。

你应该检查,如果你点击模式覆盖,而不是更容易。

您的模板:

 <div #modalOverlay (click)="clickOutside($event)" class="modal fade show" role="dialog" style="display: block;"> <div class="modal-dialog" [ngClass]='size' role="document"> <div class="modal-content" id="modal-content"> <div class="close-modal" (click)="closeModal()"> <i class="fa fa-times" aria-hidden="true"></i></div> <ng-content></ng-content> </div> </div> </div> 

和方法:

  @ViewChild('modalOverlay') modalOverlay: ElementRef; // ... your constructor and other method clickOutside(event: Event) { const target = event.target || event.srcElement; console.log('click', target); console.log("outside???", this.modalOverlay.nativeElement == event.target) // const isClickOutside = !this.modalBody.nativeElement.contains(event.target); // console.log("click outside ?", isClickOutside); if ("isClickOutside") { // this.closeModal(); } } 

如果您正在使用Bootstrap,则可以通过下拉(Bootstrap组件)直接使用引导程序进行。

 <div class="input-group"> <div class="input-group-btn"> <button aria-expanded="false" aria-haspopup="true" class="btn btn-default dropdown-toggle" data-toggle="dropdown" type="button"> Toggle Drop Down. <span class="fa fa-sort-alpha-asc"></span> </button> <ul class="dropdown-menu"> <li>List 1</li> <li>List 2</li> <li>List 3</li> </ul> </div> </div> 

现在可以在button上放置(click)="clickButton()"东西。 http://getbootstrap.com/javascript/#dropdowns

我没有做任何解决方法。 我刚刚附加文件:点击我的切换function如下:


     @指示({
      select器:'[appDropDown]'
     })
    导出类DropdownDirective实现OnInit {

       @HostBinding('class.open')isOpen:boolean;

      构造函数(private elemRef:ElementRef){}

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

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

所以,当我在我的指令之外时,我closures了下拉菜单。

我想补充@Tony的回答,因为事件在点击组件之后没有被删除。 完整收据:

  • 用#container标记您的主要元素

     @ViewChild('container') container; _dropstatus: boolean = false; get dropstatus() { return this._dropstatus; } set dropstatus(b: boolean) { if (b) { document.addEventListener('click', this.offclickevent);} else { document.removeEventListener('click', this.offclickevent);} this._dropstatus = b; } offclickevent: any = ((evt:any) => { if (!this.container.nativeElement.contains(evt.target)) this.dropstatus= false; }).bind(this); 
  • 在可点击的元素上,使用:

     (click)="dropstatus=true" 

现在,您可以使用dropstatusvariables控制下拉状态,并使用[ngClass]应用适当的类…

一个更好的@Tony版本很好的解决scheme:

 @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.closest(".ourDropdown.open").classList.remove("open"); } } } 

在一个CSS文件中:/ /如果你使用引导下拉菜单不需要。

 .ourDropdown{ display: none; } .ourDropdown.open{ display: inherit; } 

你可以写指令:

 @Directive({ selector: '[clickOut]' }) export class ClickOutDirective implements AfterViewInit { @Input() clickOut: boolean; @Output() clickOutEvent: EventEmitter<any> = new EventEmitter<any>(); @HostListener('document:mousedown', ['$event']) onMouseDown(event: MouseEvent) { if (this.clickOut && !event.path.includes(this._element.nativeElement)) { this.clickOutEvent.emit(); } } } 

在你的组件中:

 @Component({ selector: 'app-root', template: ` <h1 *ngIf="isVisible" [clickOut]="true" (clickOutEvent)="onToggle()" >{{title}}</h1> `, styleUrls: ['./app.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class AppComponent { title = 'app works!'; isVisible = false; onToggle() { this.isVisible = !this.isVisible; } } 

当html元素包含在DOM中并且[clickOut] input属性为'true'时,该指令发出事件。 它会监听mousedown事件来处理元素将从DOM中删除之前的事件。

还有一个注意:firefox不包含属性'path',你可以使用function来创buildpath:

 const getEventPath = (event: Event): HTMLElement[] => { if (event['path']) { return event['path']; } if (event['composedPath']) { return event['composedPath'](); } const path = []; let node = <HTMLElement>event.target; do { path.push(node); } while (node = node.parentElement); return path; }; 

所以你应该改变指令的事件处理程序:event.path应该被replace成getEventPath(event)

这个模块可以帮助。 https://www.npmjs.com/package/ngx-clickout它包含相同的逻辑,但也在源html元素上处理esc事件。;

我也做了一些我自己的解决方法。

我在我的ng-select元素组件中创build了一个(dropdownOpen)事件,并调用一个将closures除当前打开的SelectComponent之外打开的所有其他SelectComponent的函数。

我在下面的select.ts文件中修改了一个函数来发出事件:

 private open():void { this.options = this.itemObjects .filter((option:SelectItem) => (this.multiple === false || this.multiple === true && !this.active.find((o:SelectItem) => option.text === o.text))); if (this.options.length > 0) { this.behavior.first(); } this.optionsOpened = true; this.dropdownOpened.emit(true); } 

在HTML中我添加了一个事件监听器(dropdownOpened)

 <ng-select #elem (dropdownOpened)="closeOtherElems(elem)" [multiple]="true" [items]="items" [disabled]="disabled" [isInputAllowed]="true" (data)="refreshValue($event)" (selected)="selected($event)" (removed)="removed($event)" placeholder="No city selected"></ng-select> 

这是我在具有ng2-select标签的组件内的事件触发器的调用函数:

 @ViewChildren(SelectComponent) selectElem :QueryList<SelectComponent>; public closeOtherElems(element){ let a = this.selectElem.filter(function(el){ return (el != element) }); a.forEach(function(e:SelectComponent){ e.closeDropdown(); }) }