首页 文章

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

提问于
浏览
113

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

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

这是我的实施:

下拉组件:

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

  • 每次将此组件设置为可见时(例如:当用户单击按钮以显示它时),它会订阅存储在SubjectsService中的"global" rxjs主题userMenu .

  • 每次隐藏时,都会取消订阅此主题 .

  • 每次点击任何地方 within 此组件的模板都会触发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({});
    }
}

困扰我的是什么:

  • 我觉得让全局主题充当这些组件之间的连接器的想法让我感到很自在 .

  • setTimeout :这是必需的,因为如果用户单击显示下拉列表的按钮,则会发生以下情况:

  • 用户单击按钮(不是下拉组件的一部分)以显示下拉列表 .

  • 显示下拉列表并 it immediately subscribe to the userMenu subject .

  • 点击事件冒泡到应用程序组件并被捕获

  • 应用程序组件在 userMenu 主题上发出事件

  • 下拉组件在 userMenu 上捕获此操作并隐藏下拉列表 .

  • 最后,永远不会显示下拉列表 .

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

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

17 回答

  • 7

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

    @Component({
      host: {
        '(document:click)': 'onClick($event)',
      },
    })
    class SomeComponent() {
      constructor(private _eref: ElementRef) { }
    
      onClick(event) {
       if (!this._eref.nativeElement.contains(event.target)) // or some similar check
         doSomething();
      }
    }
    

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

  • 2

    优雅方法

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

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

    close 是你的函数,当用户点击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);
            }
        }
    }
    
  • 2

    我这样做了 .

    在文档 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";
            }
        }
    }
    
  • 1

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

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

  • 3

    我们的问题是因为我们不想点击其他组件或指令,而只是在特定div之外 .

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

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

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

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

    3.)在我们的控制器中,我们创建了我们想要在clickout事件上调用的方法,并且我们使用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>
    
  • 17

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

    从Angular 4开始, HostListener decorate是首选方法

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

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

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

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

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

  • 0

    我想补充@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"
    

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

  • 2

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

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

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

  • 0

    正确的答案有一个问题,如果你的popover中有一个clicakble组件,该元素将不再在 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();
        }
      }
    }
    

    谢谢您的帮助!

  • 0
    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: - 在页面上为每个这些组件点击两次事件监听器 . 不要在页面上的组件上使用它数百次 .

  • 1

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

    你的模板:

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

    如果您使用的是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>
    

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

  • 7

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

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

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

    你可以写指令:

    @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]输入属性为'true'时,该指令发出事件 . 它会监听mousedown事件,以便在从DOM中删除元素之前处理事件 .

    还有一点需要注意: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;
    };
    

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

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

  • 0

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

    我创建了一个 (dropdownOpen) 事件,我在ng-select元素组件中监听并调用一个函数,该函数将关闭除当前打开的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();
        })
    }
    
  • 22

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

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

    Copy/Paste from the above link:

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

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

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

    What i did:

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

    这是你的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>
    

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

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

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

相关问题