简体   繁体   English

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

[英]How can I close a dropdown on click outside?

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.我对使用全局 Subject 作为这些组件之间的连接器的想法感到不太舒服。
  2. The setTimeout : This is needed because here is what happen otherwise if the user click on the button that show the dropdown: setTimeout :这是必需的,因为如果用户单击显示下拉列表的按钮,则会发生以下情况:
    • 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.最后,下拉列表永远不会显示。

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:) !如果您知道更清洁、更好、更智能、更快或更强大的解决方案,请告诉我:)!

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 的这些帖子:

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).我检查了一下,效果很好(我只将clickOutside.directive.ts复制到我的项目中)。 U can use it in this way:你可以这样使用它:

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

Where close is your function which will be call when user click outside div. close是您的函数,当用户在 div 外单击时将调用该函数。 It is very elegant way to handle problem described in question.这是处理问题中描述的问题的非常优雅的方式。

If you use above directive to close popUp window, remember first to add event.stopPropagation() to button click event handler which open popUp.如果您使用上述指令关闭弹出窗口,请记住首先将event.stopPropagation()添加到打开弹出窗口的按钮单击事件处理程序。

BONUS:奖金:

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复制原始指令代码(以防链接将来停止工作) - 作者是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); } } }

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.在文档click添加了一个事件侦听器,并在该处理程序中检查我的container包含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";
        }
    }
}

I think Sasxa accepted answer works for most people.我认为 Sasxa 接受的答案适用于大多数人。 However, I had a situation, where the content of the Element, that should listen to off-click events, changed dynamically.但是,我遇到了一种情况,即应该侦听关闭点击事件的 Element 的内容会动态更改。 So the Elements nativeElement did not contain the event.target, when it was created dynamically.因此,当动态创建时,Elements nativeElement 不包含 event.target。 I could solve this with the following directive我可以用以下指令解决这个问题

@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.我没有检查 elementRef 是否包含 event.target,而是检查 elementRef 是否在事件的路径(到目标的 DOM 路径)中。 That way it is possible to handle dynamically created Elements.这样就可以处理动态创建的元素。

If you're doing this on iOS, use the touchstart event as well:如果您在 iOS 上执行此操作,也请使用touchstart事件:

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

}

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.我们今天在工作中一直在研究类似的问题,试图弄清楚如何让下拉 div 在被点击时消失。 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.我们的问题与最初发布者的问题略有不同,因为我们不想点击不同的组件指令,而只是在特定的 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. 1.) 我们给整个下拉菜单 div 一个唯一的类名。

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. 2.) 在内部下拉菜单本身(我们希望点击不关闭菜单的唯一部分),我们添加了一个 (window:mouseup) 事件处理程序并传入 $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. 3.) 在我们的控制器中,我们创建了我们想要在点击事件上调用的方法,我们使用 event.closest( 此处的文档)来确定点击的位置是否在我们的目标类 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>

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.因此,当我超出指令范围时,我会关闭下拉菜单。

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. z-index 需要足够高以将其定位在除下拉列表之外的所有内容之上。 In this case my dropdown would b z-index 2.在这种情况下,我的下拉列表将 b 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.我想我不会用丝网印刷的方式来解决这个问题。

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.不要在页面上数百次的组件上使用它。

You can use mouseleave in your view like this您可以像这样在视图中使用mouseleave

Test with angular 8 and work perfectly使用 angular 8 进行测试并完美运行

<ul (mouseleave)="closeDropdown()"> </ul>

I would like to complement @Tony answer, since the event is not being removed after the click outside the component.我想补充@Tony 的答案,因为在组件外单击后该事件不会被删除。 Complete receipt:完整收据:

  • 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] 应用适当的类...

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'.当 html 元素包含在 DOM 中并且 [clickOut] 输入属性为“true”时,此指令会发出事件。 It listen mousedown event to handle event before element will be removed from DOM.在元素从 DOM 中移除之前,它会监听 mousedown 事件来处理事件。

And one note: firefox doesn't contain property 'path' on event you can use function to create path:一个注意事项:firefox 不包含事件的属性“路径”,您可以使用函数创建路径:

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 事件。

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:正确答案有问题,如果您的弹出窗口中有可点击的组件,则该元素将不再位于contain方法上并将关闭,基于我自己创建的 @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!谢谢您的帮助!

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 文件中://如果您使用引导下拉菜单,则不需要。

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

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

If you are using Bootstrap, you can do it directly with bootstrap way via dropdowns (Bootstrap component).如果您使用的是 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.现在可以将(click)="clickButton()"放在按钮上。 http://getbootstrap.com/javascript/#dropdowns http://getbootstrap.com/javascript/#dropdowns

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

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://stackoverflow.com/questions/47571144我在这里回答了同样的问题: https : //stackoverflow.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);我做了一个单独的固定 div,高度 100%,宽度 100% 和 transform:scale(0),这本质上是背景,你可以用 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.菜单的 z-index 高于其他所有内容,然后背景 div 的 z-index 低于菜单但也高于其他所有内容。 Then the background has a click event that close the drop-down.然后背景有一个关闭下拉菜单的点击事件。

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

I came across to another solution, inspired by examples with focus/blur event.我遇到了另一个解决方案,灵感来自于焦点/模糊事件的例子。

So, if you want to achieve the same functionality without attaching global document listener, you may consider as a valid the following example.因此,如果您想在不附加全局文档侦听器的情况下实现相同的功能,您可以将以下示例视为有效。 It works also in Safari and Firefox on OSx, despite they have other handling of button focus event: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus它也适用于 OSx 上的 Safari 和 Firefox,尽管它们对按钮焦点事件有其他处理: https : //developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus

Working example on stackbiz with angular 8: https://stackblitz.com/edit/angular-sv4tbi?file=src%2Ftoggle-dropdown%2Ftoggle-dropdown.directive.ts使用 angular 8 的 stackbiz 上的工作示例: https ://stackblitz.com/edit/angular-sv4tbi?file = src%2Ftoggle-dropdown%2Ftoggle-dropdown.directive.ts

HTML markup: HTML 标记:

<div class="dropdown">
  <button class="btn btn-secondary dropdown-toggle" type="button" aria-haspopup="true" aria-expanded="false">Dropdown button</button>
  <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
    <a class="dropdown-item" href="#">Action</a>
    <a class="dropdown-item" href="#">Another action</a>
    <a class="dropdown-item" href="#">Something else here</a>
  </div>
</div>

The directive will look like this:该指令将如下所示:

import { Directive, HostBinding, ElementRef, OnDestroy, Renderer2 } from '@angular/core';

@Directive({
  selector: '.dropdown'
})
export class ToggleDropdownDirective {

  @HostBinding('class.show')
  public isOpen: boolean;

  private buttonMousedown: () => void;
  private buttonBlur: () => void;
  private navMousedown: () => void;
  private navClick: () => void;

  constructor(private element: ElementRef, private renderer: Renderer2) { }

  ngAfterViewInit() {
    const el = this.element.nativeElement;
    const btnElem = el.querySelector('.dropdown-toggle');
    const menuElem = el.querySelector('.dropdown-menu');

    this.buttonMousedown = this.renderer.listen(btnElem, 'mousedown', (evt) => {
      console.log('MOUSEDOWN BTN');
      this.isOpen = !this.isOpen;
      evt.preventDefault(); // prevents loose of focus (default behaviour) on some browsers
    });

    this.buttonMousedown = this.renderer.listen(btnElem, 'click', () => {
      console.log('CLICK BTN');
      // firefox OSx, Safari, Ie OSx, Mobile browsers.
      // Whether clicking on a <button> causes it to become focused varies by browser and OS.
      btnElem.focus();
    });

    // only for debug
    this.buttonMousedown = this.renderer.listen(btnElem, 'focus', () => {
      console.log('FOCUS BTN');
    });

    this.buttonBlur = this.renderer.listen(btnElem, 'blur', () => {
      console.log('BLUR BTN');
      this.isOpen = false;
    });

    this.navMousedown = this.renderer.listen(menuElem, 'mousedown', (evt) => {
      console.log('MOUSEDOWN MENU');
      evt.preventDefault(); // prevents nav element to get focus and button blur event to fire too early
    });
    this.navClick = this.renderer.listen(menuElem, 'click', () => {
      console.log('CLICK MENU');
      this.isOpen = false;
      btnElem.blur();
    });
  }

  ngOnDestroy() {
    this.buttonMousedown();
    this.buttonBlur();
    this.navMousedown();
    this.navClick();
  }
}

I decided to post my own solution based on my use case.我决定根据我的用例发布我自己的解决方案。 I have a href with a (click) event in Angular 11. This toggles a menu component in the main app.ts on off/我在 Angular 11 中有一个带有(点击)事件的 href。这将主 app.ts 中的菜单组件切换为 off/

<li><a href="javascript:void(0)" id="menu-link" (click)="toggleMenu();" ><img id="menu-image" src="img/icons/menu-white.png" ></a></li>

The menu component (eg div) is visible (*ngIf) based on a boolean named "isMenuVisible".菜单组件(例如 div)基于名为“isMenuVisible”的布尔值是可见的(*ngIf)。 And of course it can be a dropdown or any component.当然,它可以是下拉列表或任何组件。

In the app.ts I have this simple function在 app.ts 我有这个简单的功能

@HostListener('document:click', ['$event'])
onClick(event: Event) {

    const elementId = (event.target as Element).id;
    if (elementId.includes("menu")) {
        return;
    }

    this.isMenuVisble = false;

}

This means that clicking anywhere outside the "named" context closes/hides the "named" component.这意味着单击“命名”上下文之外的任何地方都会关闭/隐藏“命名”组件。

Super complicated.超级复杂。 I read them but not possible to reproduce them with my code.我阅读了它们,但无法用我的代码重现它们。 I have this code for a dropdown menu in java.我在 java 中有一个下拉菜单的代码。

document.addEventListener("mouseover", e => { 
  const isDropdownButton = e.target.matches("[data-dropdown-button]")
  if (!isDropdownButton && e.closest('[data-dropdown]') != null) return
  
  let currentDropDown
  if (isDropdownButton) {
    currentDropdown = e.target.closest('[data-dropdown]')
    currentDropdown.classList.toggle('active')
  }
  
  document.querySelectorAll("[data-dropdown].active").forEach(dropdown => {
    if (dropdown === currentDropdown) return
    dropdown.classList.remove("active")
  })
})

which is working well, as mouse hover opens the dropdown and keeps that open.效果很好,因为鼠标 hover 打开下拉菜单并保持打开状态。 But there are two missing functions.但是缺少两个功能。

  1. When I click somewhere else, dropdown does not close.当我点击其他地方时,下拉菜单不会关闭。
  2. When I click on the dropdown menu, go to an URL address.当我点击下拉菜单时,go 到 URL 地址。 Thanks谢谢

I've made a directive to address this similar problem and I'm using Bootstrap.我已经制定了一个指令来解决这个类似的问题,我正在使用 Bootstrap。 But in my case, instead of waiting for the click event outside the element to close the current opened dropdown menu I think it is better if we watch over the 'mouseleave' event to automatically close the menu.但在我的例子中,与其等待元素外的点击事件关闭当前打开的下拉菜单,我认为如果我们监视 'mouseleave' 事件以自动关闭菜单会更好。

Here's my solution:这是我的解决方案:

Directive指示

import { Directive, HostListener, HostBinding } from '@angular/core';
@Directive({
  selector: '[appDropdown]'
})
export class DropdownDirective {

  @HostBinding('class.open') isOpen = false;

  @HostListener('click') toggleOpen() {
    this.isOpen = !this.isOpen;
  }

  @HostListener('mouseleave') closeDropdown() {
    this.isOpen = false;
  }

}

HTML HTML

<ul class="nav navbar-nav navbar-right">
    <li class="dropdown" appDropdown>
      <a class="dropdown-toggle" data-toggle="dropdown">Test <span class="caret"></span>
      </a>
      <ul class="dropdown-menu">
          <li routerLinkActive="active"><a routerLink="/test1">Test1</a></li>
          <li routerLinkActive="active"><a routerLink="/test2/">Test2</a></li>
      </ul>
    </li>
</ul>

THE MOST ELEGANT METHOD :D最优雅的方法:D

There is one easiest way to do that, no need any directives for that.有一种最简单的方法可以做到这一点,不需要任何指令。

"element-that-toggle-your-dropdown" should be button tag. “element-that-toggle-your-dropdown”应该是按钮标签。 Use any method in (blur) attribute.在 (blur) 属性中使用任何方法。 That's all.就这样。

<button class="element-that-toggle-your-dropdown"
               (blur)="isDropdownOpen = false"
               (click)="isDropdownOpen = !isDropdownOpen">
</button>

This is the Angular Bootstrap DropDowns button sample with close outside of component.这是组件外部关闭的 Angular Bootstrap DropDowns 按钮示例。

without use bootstrap.js不使用bootstrap.js

// .html
<div class="mx-3 dropdown" [class.show]="isTestButton">
  <button class="btn dropdown-toggle"
          (click)="isTestButton = !isTestButton">
    <span>Month</span>
  </button>
  <div class="dropdown-menu" [class.show]="isTestButton">
    <button class="btn dropdown-item">Month</button>
    <button class="btn dropdown-item">Week</button>
  </div>
</div>

// .ts
import { Component, ElementRef, HostListener } from "@angular/core";

@Component({
  selector: "app-test",
  templateUrl: "./test.component.html",
  styleUrls: ["./test.component.scss"]
})
export class TestComponent {

  isTestButton = false;

  constructor(private eleRef: ElementRef) {
  }


  @HostListener("document:click", ["$event"])
  docEvent($e: MouseEvent) {
    if (!this.isTestButton) {
      return;
    }
    const paths: Array<HTMLElement> = $e["path"];
    if (!paths.some(p => p === this.eleRef.nativeElement)) {
      this.isTestButton = false;
    }
  }
}

I didn't think there were enough answers so I want to pitch in. Here's what I did我认为没有足够的答案,所以我想参与。这就是我所做的

component.ts组件.ts

@Component({
    selector: 'app-issue',
    templateUrl: './issue.component.html',
    styleUrls: ['./issue.component.sass'],
})
export class IssueComponent {
    @Input() issue: IIssue;
    @ViewChild('issueRef') issueRef;
    
    public dropdownHidden = true;
    
    constructor(private ref: ElementRef) {}

    public toggleDropdown($event) {
        this.dropdownHidden = !this.dropdownHidden;
    }
    
    @HostListener('document:click', ['$event'])
    public hideDropdown(event: any) {
        if (!this.dropdownHidden && !this.issueRef.nativeElement.contains(event.target)) {
            this.dropdownHidden = true;
        }
    }
}

component.html组件.html

<div #issueRef (click)="toggleDropdown()">
    <div class="card card-body">
        <p class="card-text truncate">{{ issue.fields.summary }}</p>
        <div class="d-flex justify-content-between">
            <img
                *ngIf="issue.fields.assignee; else unassigned"
                class="rounded"
                [src]="issue.fields.assignee.avatarUrls['32x32']"
                [alt]="issue.fields.assignee.displayName"
            />
            <ng-template #unassigned>
                <img
                    class="rounded"
                    src="https://img.icons8.com/pastel-glyph/2x/person-male--v2.png"
                    alt="Unassigned"
                />
            </ng-template>
            <img
                *ngIf="issue.fields.priority"
                class="rounded mt-auto priority"
                [src]="issue.fields.priority.iconUrl"
                [alt]="issue.fields.priority.name"
            />
        </div>
    </div>
    <div *ngIf="!dropdownHidden" class="list-group context-menu">
        <a href="#" class="list-group-item list-group-item-action active" aria-current="true">
            The current link item
        </a>
        <a href="#" class="list-group-item list-group-item-action">A second link item</a>
        <a href="#" class="list-group-item list-group-item-action">A third link item</a>
        <a href="#" class="list-group-item list-group-item-action">A fourth link item</a>
        <a
            href="#"
            class="list-group-item list-group-item-action disabled"
            tabindex="-1"
            aria-disabled="true"
            >A disabled link item</a
        >
    </div>
</div>

You can use the (click) event on the document and check if the event target is not within the dropdown container.您可以在文档上使用 (click) 事件并检查事件目标是否不在下拉容器内。 If it's not, then you can close the dropdown.如果不是,则可以关闭下拉菜单。 Here's an example:这是一个例子:

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

@Component({
  selector: 'app-root',
  template: `
    <div #container>
      <button (click)="toggleDropdown()">Toggle Dropdown</button>
      <div *ngIf="isDropdownOpen">
        Dropdown Content
      </div>
    </div>
  `,
  styles: []
})
export class AppComponent implements OnInit {
  @ViewChild('container', { static: true }) container: ElementRef;
  isDropdownOpen = false;

  ngOnInit() {
    document.addEventListener('click', (event) => {
      if (!this.container.nativeElement.contains(event.target)) {
        this.isDropdownOpen = false;
      }
    });
  }

  toggleDropdown() {
    this.isDropdownOpen = !this.isDropdownOpen;
  }
}

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

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