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

时间:2021-12-09 07:55:07

I would like to close my login menu dropdown when the user click anywhere outside of that dropdown, and I'd like to do that with Angular2 and with the Angular2 "approach"...

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

I have implemented a solution, but I really do not feel confident with it. I think there must be an easiest way to achieve the same result, so if you have any ideas ... let's discuss :) !

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

Here is my implementation:

这是我的实施:

The dropdown component:

This is the component for my dropdown:

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

  • Every time this component it set to visible, (For example: when the user click on a button to display it) it subscribe to a "global" rxjs subject userMenu stored within the SubjectsService.
  • 每次将此组件设置为可见时(例如:当用户单击按钮以显示它时),它会订阅存储在SubjectsService中的“全局”rxjs主题userMenu。
  • And every time it is hidden, it unsubscribe to this subject.
  • 每次隐藏时,都会取消订阅此主题。
  • Every click anywhere within the template of this component trigger the onClick() method, which just stop event bubbling to the top (and the application component)
  • 在该组件的模板中的任何位置的每次单击都会触发onClick()方法,该方法只是将事件冒泡停止到顶部(和应用程序组件)

Here is the code

这是代码

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

The application component:

On the other hand, there is the application component (which is a parent of the dropdown component):

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

  • This component catch every click event and emit on the same rxjs Subject (userMenu)
  • 此组件捕获每个单击事件并在相同的rxjs上发出主题(userMenu)

Here is the code:

这是代码:

export class AppComponent {

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

What bother me:

  1. I do not feel really comfortable with the idea of having a global Subject that act as the connector between those components.
  2. 我觉得让一个全局主题充当这些组件之间的连接器的想法让我感到很自在。
  3. The setTimeout: This is needed because here is what happen otherwise if the user click on the button that show the dropdown:
    • The user click on the button (which is not a part of the dropdown component) to show the dropdown.
    • 用户单击按钮(不是下拉组件的一部分)以显示下拉列表。
    • The dropdown is displayed and it immediately subscribe to the userMenu subject.
    • 将显示下拉列表,并立即订阅userMenu主题。
    • The click event bubble up to the app component and gets caught
    • 点击事件会冒泡到应用程序组件并被捕获
    • The application component emit an event on the userMenu subject
    • 应用程序组件在userMenu主题上发出事件
    • The dropdown component catch this action on userMenu and hide the dropdown.
    • 下拉组件在userMenu上捕获此操作并隐藏下拉列表。
    • At the end the dropdown is never displayed.
    • 最后,永远不会显示下拉列表。
  4. setTimeout:这是必需的,因为如果用户单击显示下拉列表的按钮,则会发生以下情况:用户单击按钮(不是下拉组件的一部分)以显示下拉列表。将显示下拉列表,并立即订阅userMenu主题。 click事件冒泡到应用程序组件并被捕获应用程序组件在userMenu主题上发出事件下拉组件在userMenu上捕获此操作并隐藏下拉列表。最后,永远不会显示下拉列表。

This set timeout delay the subscription to the end of the current JavaScript code turn which solve the problem, but in a very in elegant way in my opinion.

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

If you know cleaner, better, smarter, faster or stronger solutions, please let me know :) !

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

17 个解决方案

#1


181  

You can use (document:click) event:

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

Another approach is to create custom event as a directive. Check out these posts by Ben Nadel:

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

#2


16  

I've done it this way.

我这样做了。

Added an event listener on document click and in that handler checked if my container contains event.target, if not - hide the dropdown.

在文档点击时添加了一个事件监听器,并在该处理程序中检查我的容器是否包含event.target,如果没有 - 隐藏下拉列表。

It would look like this.

它看起来像这样。

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

#3


14  

ELEGANT METHOD: I found this clickOut directive: https://github.com/chliebel/angular2-click-outside

优雅方法:我发现了这个clickOut指令:https://github.com/chliebel/angular2-click-outside

I check it and it works well (i only copy clickOutside.directive.ts to my project). U can use it in this way:

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

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

Where close is your function which will be call when user click outside div. It is very elegant way to handle problem described in question.

您的功能关闭,当用户点击div外部时将调用。处理问题所描述的问题是非常优雅的方式。

=== Directive code ===

===指令代码===

Below i copy oryginal directive code from file clickOutside.directive.ts (in case if link will stop working in future) - the author is Christian Liebel :

下面我从文件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);
        }
    }
}

#4


9  

I think Sasxa accepted answer works for most people. However, I had a situation, where the content of the Element, that should listen to off-click events, changed dynamically. So the Elements nativeElement did not contain the event.target, when it was created dynamically. I could solve this with the following directive

我认为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);
    }
  }
}

Instead of checking if elementRef contains event.target, I check if elementRef is in the path (DOM path to target) of the event. That way it is possible to handle dynamically created Elements.

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

#5


7  

We've been working on a similar issue at work today, trying to figure out how to make a dropdown div disappear when it is clicked off of. Ours is slightly different than the initial poster's question because we didn't want to click away from a different component or directive, but merely outside of the particular div.

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

We ended up solving it by using the (window:mouseup) event handler.

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

Steps:
1.) We gave the entire dropdown menu div a unique class name.

2.) On the inner dropdown menu itself (the only portion that we wanted clicks to NOT close the menu), we added a (window:mouseup) event handler and passed in the $event.

NOTE: It could not be done with a typical "click" handler because this conflicted with the parent click handler.

3.) In our controller, we created the method that we wanted to be called on the click out event, and we use the event.closest (docs here) to find out if the clicked spot is within our targeted-class div.

步骤: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
        }
    }
 <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>

#6


6  

If you're doing this on iOS, use the touchstart event as well:

As of Angular 4, the HostListener decorate is the preferred way to do this

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

}

#7


3  

You could create a sibling element to the dropdown that covers the entire screen that would be invisible and be there just for capturing click events. Then you could detect clicks on that element and close the dropdown when it is clicked. Lets say that element is of class silkscreen, here is some style for it:

您可以在下拉列表中创建一个兄弟元素,该元素覆盖整个屏幕,该屏幕将是不可见的,仅用于捕获点击事件。然后,您可以检测该元素上的点击,并在单击时关闭下拉列表。让我们说元素是丝网印刷类,这里有一些风格:

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

The z-index needs to be high enough to position it above everything but your dropdown. In this case my dropdown would b z-index 2.

z-index必须足够高才能将其定位在除下拉列表之外的所有内容上。在这种情况下,我的下拉列表将是z-index 2。

The other answers worked in some cases for me, except sometimes my dropdown closed when I interacted with elements within it and I didn't want that. I had dynamically added elements who were not contained in my component, according to the event target, like I expected. Rather than sorting that mess out I figured I'd just try it the silkscreen way.

其他答案在某些情况下适用于我,除非有时候当我与其中的元素进行交互时我的下拉关闭,我不想这样做。根据事件目标,我动态添加了未包含在我的组件中的元素,就像我预期的那样。我认为我只是尝试丝网印刷方式,而不是整理那些混乱。

#8


2  

I would like to complement @Tony answer, since the event is not being removed after the click outside the component. Complete receipt:

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

  • Mark your main element with #container

    使用#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);
    
  • On the clickable element, use:

    在可点击元素上,使用:

    (click)="dropstatus=true"
    

Now you can control your dropdown state with the dropstatus variable, and apply proper classes with [ngClass]...

现在,您可以使用dropstatus变量控制下拉状态,并使用[ngClass]应用适当的类...

#9


1  

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: - Two click event listeners for every one of these components on page. Don't use this on components that are on the page hundreds of times.

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

#10


1  

You should check if you click on the modal overlay instead, a lot easier.

您应该检查是否单击模态叠加,更容易。

Your template:

你的模板:

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

And the method:

方法:

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


  }

#11


1  

If you are using Bootstrap, you can do it directly with bootstrap way via dropdowns (Bootstrap component).

如果您使用的是Bootstrap,则可以通过下拉列表(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>

Now it's OK to put (click)="clickButton()" stuff on the button. http://getbootstrap.com/javascript/#dropdowns

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

#12


1  

I didn't make any workaround. I've just attached document:click on my toggle function as follow :

我没有做任何解决方法。我刚刚附上了文档:点击我的切换功能如下:


    @Directive({
      selector: '[appDropDown]'
    })
    export class DropdownDirective implements OnInit {

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

      constructor(private elemRef: ElementRef) { }

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

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

So, when I am outside my directive, I close the dropdown.

所以,当我在我的指令之外时,我会关闭下拉列表。

#13


1  

The correct answer has a problem, if you have a clicakble component in your popover, the element will no longer on the contain method and will close, based on @JuHarm89 i created my own:

正确的答案有一个问题,如果你的popover中有一个clicakble组件,该元素将不再在contains方法上并将关闭,基于@ JuHarm89我创建了自己的:

export class PopOverComponent implements AfterViewInit {
 private parentNode: any;

  constructor(
    private _element: ElementRef
  ) { }

  ngAfterViewInit(): void {
    this.parentNode = this._element.nativeElement.parentNode;
  }

  @HostListener('document:click', ['$event.path'])
  onClickOutside($event: Array<any>) {
    const elementRefInPath = $event.find(node => node === this.parentNode);
    if (!elementRefInPath) {
      this.closeEventEmmit.emit();
    }
  }
}

Thanks for the help!

谢谢您的帮助!

#14


0  

A better version for @Tony great solution:

@Tony伟大解决方案的更好版本:

@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");

        }
    }
}

In a css file: //NOT needed if you use bootstrap drop-down.

在css文件中://如果使用bootstrap下拉列表,则不需要。

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

#15


0  

You can write directive:

你可以写指令:

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


}

In your component:

在您的组件中:

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

This directive emit event when html element is containing in DOM and when [clickOut] input property is 'true'. It listen mousedown event to handle event before element will be removed from DOM.

当html元素包含在DOM中并且[clickOut]输入属性为“true”时,该指令会发出事件。它会监听mousedown事件,以便在从DOM中删除元素之前处理事件。

And one note: firefox doesn't contain property 'path' on event you can use function to create path:

还有一点需要注意:firefox在事件上不包含属性'path',你可以使用函数来创建路径:

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

So you should change event handler on the directive: event.path should be replaced getEventPath(event)

所以你应该在指令上更改事件处理程序:event.path应该被替换为getEventPath(event)

This module can help. https://www.npmjs.com/package/ngx-clickout It contains the same logic but also handle esc event on source html element.

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

#16


0  

I also did a little workaround of my own.

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

I created a (dropdownOpen) event which I listen to at my ng-select element component and call a function which will close all the other SelectComponent's opened apart from the currently opened SelectComponent.

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

I modified one function inside the select.ts file like below to emit the event:

我在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);
}

In the HTML I added an event listener for (dropdownOpened):

在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>

This is my calling function on event trigger inside the component having ng2-select tag:

这是我在具有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();
    })
}

#17


0  

NOTE: For those wanting to use web workers and you need to avoid using document and nativeElement this will work.

注意:对于那些想要使用Web worker并且您需要避免使用document和nativeElement的人来说,这将起作用。

I answered the same question here: https://*.com/questions/47571144

我在这里回答了同样的问题:https://*.com/questions/47571144

Copy/Paste from the above link:

从以上链接复制/粘贴:

I had the same issue when I was making a drop-down menu and a confirmation dialog I wanted to dismiss them when clicking outside.

我在制作下拉菜单和确认对话框时遇到了同样的问题,我想在外面点击时将其解雇。

My final implementation works perfectly but requires some css3 animations and styling.

我的最终实现完美无缺,但需要一些css3动画和样式。

NOTE: i have not tested the below code, there may be some syntax problems that need to be ironed out, also the obvious adjustments for your own project!

注意:我没有测试下面的代码,可能有一些需要解决的语法问题,也是对你自己的项目的明显调整!

What i did:

我做了什么:

I made a separate fixed div with height 100%, width 100% and transform:scale(0), this is essentially the background, you can style it with background-color: rgba(0, 0, 0, 0.466); to make obvious the menu is open and the background is click-to-close. The menu gets a z-index higher than everything else, then the background div gets a z-index lower than the menu but also higher than everything else. Then the background has a click event that close the drop-down.

我做了一个单独的固定div,高度100%,宽度100%和变换:scale(0),这基本上是背景,你可以用背景颜色设置它:rgba(0,0,0,0.466);显而易见,菜单是打开的,背景是点击关闭。菜单获得的z-index高于其他所有菜单,然后背景div获得的z-index低于菜单,但也高于其他所有菜单。然后,后台有一个关闭下拉列表的点击事件。

Here it is with your html code.

这是你的HTML代码。

<div class="dropdownbackground" [ngClass]="{showbackground: qtydropdownOpened}" (click)="qtydropdownOpened = !qtydropdownOpened"><div>
<div class="zindex" [class.open]="qtydropdownOpened">
  <button (click)="qtydropdownOpened = !qtydropdownOpened" type="button" 
         data-toggle="dropdown" aria-haspopup="true" [attr.aria-expanded]="qtydropdownOpened ? 'true': 'false' ">
   {{selectedqty}}<span class="caret margin-left-1x "></span>
 </button>
  <div class="dropdown-wrp dropdown-menu">
  <ul class="default-dropdown">
      <li *ngFor="let quantity of quantities">
       <a (click)="qtydropdownOpened = !qtydropdownOpened;setQuantity(quantity)">{{quantity  }}</a>
       </li>
   </ul>
  </div>
 </div>

Here is the css3 which needs some simple animations.

这是css3,需要一些简单的动画。

/* make sure the menu/drop-down is in front of the background */
.zindex{
    z-index: 3;
}

/* make background fill the whole page but sit behind the drop-down, then
scale it to 0 so its essentially gone from the page */
.dropdownbackground{
    width: 100%;
    height: 100%;
    position: fixed;
    z-index: 2;
    transform: scale(0);
    opacity: 0;
    background-color: rgba(0, 0, 0, 0.466);
}

/* this is the class we add in the template when the drop down is opened
it has the animation rules set these how you like */
.showbackground{
    animation: showBackGround 0.4s 1 forwards; 

}

/* this animates the background to fill the page
if you don't want any thing visual you could use a transition instead */
@keyframes showBackGround {
    1%{
        transform: scale(1);
        opacity: 0;
    }
    100% {
        transform: scale(1);
        opacity: 1;
    }
}

If you aren't after anything visual you can just use a transition like this

如果你没有追求任何视觉效果,你可以使用这样的过渡

.dropdownbackground{
    width: 100%;
    height: 100%;
    position: fixed;
    z-index: 2;
    transform: scale(0);
    opacity: 0;
    transition all 0.1s;
}

.dropdownbackground.showbackground{
     transform: scale(1);
}

#1


181  

You can use (document:click) event:

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

Another approach is to create custom event as a directive. Check out these posts by Ben Nadel:

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

#2


16  

I've done it this way.

我这样做了。

Added an event listener on document click and in that handler checked if my container contains event.target, if not - hide the dropdown.

在文档点击时添加了一个事件监听器,并在该处理程序中检查我的容器是否包含event.target,如果没有 - 隐藏下拉列表。

It would look like this.

它看起来像这样。

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

#3


14  

ELEGANT METHOD: I found this clickOut directive: https://github.com/chliebel/angular2-click-outside

优雅方法:我发现了这个clickOut指令:https://github.com/chliebel/angular2-click-outside

I check it and it works well (i only copy clickOutside.directive.ts to my project). U can use it in this way:

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

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

Where close is your function which will be call when user click outside div. It is very elegant way to handle problem described in question.

您的功能关闭,当用户点击div外部时将调用。处理问题所描述的问题是非常优雅的方式。

=== Directive code ===

===指令代码===

Below i copy oryginal directive code from file clickOutside.directive.ts (in case if link will stop working in future) - the author is Christian Liebel :

下面我从文件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);
        }
    }
}

#4


9  

I think Sasxa accepted answer works for most people. However, I had a situation, where the content of the Element, that should listen to off-click events, changed dynamically. So the Elements nativeElement did not contain the event.target, when it was created dynamically. I could solve this with the following directive

我认为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);
    }
  }
}

Instead of checking if elementRef contains event.target, I check if elementRef is in the path (DOM path to target) of the event. That way it is possible to handle dynamically created Elements.

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

#5


7  

We've been working on a similar issue at work today, trying to figure out how to make a dropdown div disappear when it is clicked off of. Ours is slightly different than the initial poster's question because we didn't want to click away from a different component or directive, but merely outside of the particular div.

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

We ended up solving it by using the (window:mouseup) event handler.

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

Steps:
1.) We gave the entire dropdown menu div a unique class name.

2.) On the inner dropdown menu itself (the only portion that we wanted clicks to NOT close the menu), we added a (window:mouseup) event handler and passed in the $event.

NOTE: It could not be done with a typical "click" handler because this conflicted with the parent click handler.

3.) In our controller, we created the method that we wanted to be called on the click out event, and we use the event.closest (docs here) to find out if the clicked spot is within our targeted-class div.

步骤: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
        }
    }
 <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>

#6


6  

If you're doing this on iOS, use the touchstart event as well:

As of Angular 4, the HostListener decorate is the preferred way to do this

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

}

#7


3  

You could create a sibling element to the dropdown that covers the entire screen that would be invisible and be there just for capturing click events. Then you could detect clicks on that element and close the dropdown when it is clicked. Lets say that element is of class silkscreen, here is some style for it:

您可以在下拉列表中创建一个兄弟元素,该元素覆盖整个屏幕,该屏幕将是不可见的,仅用于捕获点击事件。然后,您可以检测该元素上的点击,并在单击时关闭下拉列表。让我们说元素是丝网印刷类,这里有一些风格:

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

The z-index needs to be high enough to position it above everything but your dropdown. In this case my dropdown would b z-index 2.

z-index必须足够高才能将其定位在除下拉列表之外的所有内容上。在这种情况下,我的下拉列表将是z-index 2。

The other answers worked in some cases for me, except sometimes my dropdown closed when I interacted with elements within it and I didn't want that. I had dynamically added elements who were not contained in my component, according to the event target, like I expected. Rather than sorting that mess out I figured I'd just try it the silkscreen way.

其他答案在某些情况下适用于我,除非有时候当我与其中的元素进行交互时我的下拉关闭,我不想这样做。根据事件目标,我动态添加了未包含在我的组件中的元素,就像我预期的那样。我认为我只是尝试丝网印刷方式,而不是整理那些混乱。

#8


2  

I would like to complement @Tony answer, since the event is not being removed after the click outside the component. Complete receipt:

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

  • Mark your main element with #container

    使用#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);
    
  • On the clickable element, use:

    在可点击元素上,使用:

    (click)="dropstatus=true"
    

Now you can control your dropdown state with the dropstatus variable, and apply proper classes with [ngClass]...

现在,您可以使用dropstatus变量控制下拉状态,并使用[ngClass]应用适当的类...

#9


1  

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: - Two click event listeners for every one of these components on page. Don't use this on components that are on the page hundreds of times.

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

#10


1  

You should check if you click on the modal overlay instead, a lot easier.

您应该检查是否单击模态叠加,更容易。

Your template:

你的模板:

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

And the method:

方法:

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


  }

#11


1  

If you are using Bootstrap, you can do it directly with bootstrap way via dropdowns (Bootstrap component).

如果您使用的是Bootstrap,则可以通过下拉列表(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>

Now it's OK to put (click)="clickButton()" stuff on the button. http://getbootstrap.com/javascript/#dropdowns

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

#12


1  

I didn't make any workaround. I've just attached document:click on my toggle function as follow :

我没有做任何解决方法。我刚刚附上了文档:点击我的切换功能如下:


    @Directive({
      selector: '[appDropDown]'
    })
    export class DropdownDirective implements OnInit {

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

      constructor(private elemRef: ElementRef) { }

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

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

So, when I am outside my directive, I close the dropdown.

所以,当我在我的指令之外时,我会关闭下拉列表。

#13


1  

The correct answer has a problem, if you have a clicakble component in your popover, the element will no longer on the contain method and will close, based on @JuHarm89 i created my own:

正确的答案有一个问题,如果你的popover中有一个clicakble组件,该元素将不再在contains方法上并将关闭,基于@ JuHarm89我创建了自己的:

export class PopOverComponent implements AfterViewInit {
 private parentNode: any;

  constructor(
    private _element: ElementRef
  ) { }

  ngAfterViewInit(): void {
    this.parentNode = this._element.nativeElement.parentNode;
  }

  @HostListener('document:click', ['$event.path'])
  onClickOutside($event: Array<any>) {
    const elementRefInPath = $event.find(node => node === this.parentNode);
    if (!elementRefInPath) {
      this.closeEventEmmit.emit();
    }
  }
}

Thanks for the help!

谢谢您的帮助!

#14


0  

A better version for @Tony great solution:

@Tony伟大解决方案的更好版本:

@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");

        }
    }
}

In a css file: //NOT needed if you use bootstrap drop-down.

在css文件中://如果使用bootstrap下拉列表,则不需要。

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

#15


0  

You can write directive:

你可以写指令:

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


}

In your component:

在您的组件中:

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

This directive emit event when html element is containing in DOM and when [clickOut] input property is 'true'. It listen mousedown event to handle event before element will be removed from DOM.

当html元素包含在DOM中并且[clickOut]输入属性为“true”时,该指令会发出事件。它会监听mousedown事件,以便在从DOM中删除元素之前处理事件。

And one note: firefox doesn't contain property 'path' on event you can use function to create path:

还有一点需要注意:firefox在事件上不包含属性'path',你可以使用函数来创建路径:

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

So you should change event handler on the directive: event.path should be replaced getEventPath(event)

所以你应该在指令上更改事件处理程序:event.path应该被替换为getEventPath(event)

This module can help. https://www.npmjs.com/package/ngx-clickout It contains the same logic but also handle esc event on source html element.

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

#16


0  

I also did a little workaround of my own.

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

I created a (dropdownOpen) event which I listen to at my ng-select element component and call a function which will close all the other SelectComponent's opened apart from the currently opened SelectComponent.

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

I modified one function inside the select.ts file like below to emit the event:

我在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);
}

In the HTML I added an event listener for (dropdownOpened):

在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>

This is my calling function on event trigger inside the component having ng2-select tag:

这是我在具有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();
    })
}

#17


0  

NOTE: For those wanting to use web workers and you need to avoid using document and nativeElement this will work.

注意:对于那些想要使用Web worker并且您需要避免使用document和nativeElement的人来说,这将起作用。

I answered the same question here: https://*.com/questions/47571144

我在这里回答了同样的问题:https://*.com/questions/47571144

Copy/Paste from the above link:

从以上链接复制/粘贴:

I had the same issue when I was making a drop-down menu and a confirmation dialog I wanted to dismiss them when clicking outside.

我在制作下拉菜单和确认对话框时遇到了同样的问题,我想在外面点击时将其解雇。

My final implementation works perfectly but requires some css3 animations and styling.

我的最终实现完美无缺,但需要一些css3动画和样式。

NOTE: i have not tested the below code, there may be some syntax problems that need to be ironed out, also the obvious adjustments for your own project!

注意:我没有测试下面的代码,可能有一些需要解决的语法问题,也是对你自己的项目的明显调整!

What i did:

我做了什么:

I made a separate fixed div with height 100%, width 100% and transform:scale(0), this is essentially the background, you can style it with background-color: rgba(0, 0, 0, 0.466); to make obvious the menu is open and the background is click-to-close. The menu gets a z-index higher than everything else, then the background div gets a z-index lower than the menu but also higher than everything else. Then the background has a click event that close the drop-down.

我做了一个单独的固定div,高度100%,宽度100%和变换:scale(0),这基本上是背景,你可以用背景颜色设置它:rgba(0,0,0,0.466);显而易见,菜单是打开的,背景是点击关闭。菜单获得的z-index高于其他所有菜单,然后背景div获得的z-index低于菜单,但也高于其他所有菜单。然后,后台有一个关闭下拉列表的点击事件。

Here it is with your html code.

这是你的HTML代码。

<div class="dropdownbackground" [ngClass]="{showbackground: qtydropdownOpened}" (click)="qtydropdownOpened = !qtydropdownOpened"><div>
<div class="zindex" [class.open]="qtydropdownOpened">
  <button (click)="qtydropdownOpened = !qtydropdownOpened" type="button" 
         data-toggle="dropdown" aria-haspopup="true" [attr.aria-expanded]="qtydropdownOpened ? 'true': 'false' ">
   {{selectedqty}}<span class="caret margin-left-1x "></span>
 </button>
  <div class="dropdown-wrp dropdown-menu">
  <ul class="default-dropdown">
      <li *ngFor="let quantity of quantities">
       <a (click)="qtydropdownOpened = !qtydropdownOpened;setQuantity(quantity)">{{quantity  }}</a>
       </li>
   </ul>
  </div>
 </div>

Here is the css3 which needs some simple animations.

这是css3,需要一些简单的动画。

/* make sure the menu/drop-down is in front of the background */
.zindex{
    z-index: 3;
}

/* make background fill the whole page but sit behind the drop-down, then
scale it to 0 so its essentially gone from the page */
.dropdownbackground{
    width: 100%;
    height: 100%;
    position: fixed;
    z-index: 2;
    transform: scale(0);
    opacity: 0;
    background-color: rgba(0, 0, 0, 0.466);
}

/* this is the class we add in the template when the drop down is opened
it has the animation rules set these how you like */
.showbackground{
    animation: showBackGround 0.4s 1 forwards; 

}

/* this animates the background to fill the page
if you don't want any thing visual you could use a transition instead */
@keyframes showBackGround {
    1%{
        transform: scale(1);
        opacity: 0;
    }
    100% {
        transform: scale(1);
        opacity: 1;
    }
}

If you aren't after anything visual you can just use a transition like this

如果你没有追求任何视觉效果,你可以使用这样的过渡

.dropdownbackground{
    width: 100%;
    height: 100%;
    position: fixed;
    z-index: 2;
    transform: scale(0);
    opacity: 0;
    transition all 0.1s;
}

.dropdownbackground.showbackground{
     transform: scale(1);
}