Source:  Twitter logo

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

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.
  • 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)

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)

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. 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.
    • The click event bubble up to the app component and gets caught
    • The application component emit an event on the userMenu subject
    • The dropdown component catch this action on userMenu and hide the dropdown.
    • 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.

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

You can use (document:click) event:

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

261 users liked answer #0dislike answer #0261
Sasxa profile pic
Sasxa

ELEGANT METHOD

I found this clickOut directive: 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:

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

If you use above directive to close popUp window, remember first to add event.stopPropagation() to button click event handler which open popUp.

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 :

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);
        }
    }
}
50 users liked answer #1dislike answer #150
Kamil Kiełczewski profile pic
Kamil Kiełczewski

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.

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";
        }
    }
}
19 users liked answer #2dislike answer #219
Tony profile pic
Tony

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

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

18 users liked answer #3dislike answer #318
JuHarm89 profile pic
JuHarm89

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

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

}
14 users liked answer #4dislike answer #414
Xavier profile pic
Xavier

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.

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

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.

 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>
11 users liked answer #5dislike answer #511
Paige Bolduc profile pic
Paige Bolduc

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.

6 users liked answer #6dislike answer #66
Elie Nehmé profile pic
Elie Nehmé

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.

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.

5 users liked answer #7dislike answer #75
Patrick Graham profile pic
Patrick Graham
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.

4 users liked answer #8dislike answer #84
Alex Egli profile pic
Alex Egli

You can use mouseleave in your view like this

Test with angular 8 and work perfectly

<ul (mouseleave)="closeDropdown()"> </ul>
4 users liked answer #9dislike answer #94
Tony Ngo profile pic
Tony Ngo

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

  • Mark your main element with #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]...

3 users liked answer #10dislike answer #103
Gauss profile pic
Gauss

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.

And one note: firefox doesn't contain property 'path' on event you can use function to create 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)

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.

3 users liked answer #11dislike answer #113
Alex Mikitevich profile pic
Alex Mikitevich

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:

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!

3 users liked answer #12dislike answer #123
Douglas Caina profile pic
Douglas Caina

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();
        }
    }
3 users liked answer #13dislike answer #133
Stefdelec profile pic
Stefdelec

A better version for @Tony great solution:

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

.ourDropdown{
   display: none;
}
.ourDropdown.open{
   display: inherit;
}
2 users liked answer #14dislike answer #142
Dudi profile pic
Dudi

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

<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

1 users liked answer #15dislike answer #151
vusan profile pic
vusan

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.

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

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

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

@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();
    })
}
1 users liked answer #16dislike answer #161
Gaurav Pandvia profile pic
Gaurav Pandvia

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

I answered the same question here: 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.

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.

Here it is with your html code.

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

/* 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 users liked answer #17dislike answer #171
Shannon profile pic
Shannon

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

Working example on stackbiz with angular 8: https://stackblitz.com/edit/angular-sv4tbi?file=src%2Ftoggle-dropdown%2Ftoggle-dropdown.directive.ts

HTML markup:

<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();
  }
}
1 users liked answer #18dislike answer #181
Andrei Shekhau profile pic
Andrei Shekhau

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/

<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 (e.g. div) is visible (*ngIf) based on a boolean named "isMenuVisible". And of course it can be a dropdown or any component.

In the app.ts I have this simple function

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

1 users liked answer #19dislike answer #191
Coen Damen profile pic
Coen Damen

This is the Angular Bootstrap DropDowns button sample with close outside of component.

without use 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;
    }
  }
}
1 users liked answer #20dislike answer #201
Moslem Shahsavan profile pic
Moslem Shahsavan

I've made a directive to address this similar problem and I'm using 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.

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

<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>
0 users liked answer #21dislike answer #210
Lemuel Layola Apa profile pic
Lemuel Layola Apa

THE MOST ELEGANT METHOD :D

There is one easiest way to do that, no need any directives for that.

"element-that-toggle-your-dropdown" should be button tag. Use any method in (blur) attribute. That's all.

<button class="element-that-toggle-your-dropdown"
               (blur)="isDropdownOpen = false"
               (click)="isDropdownOpen = !isDropdownOpen">
</button>
0 users liked answer #22dislike answer #220
George Reznichenko profile pic
George Reznichenko

I didn't think there were enough answers so I want to pitch in. Here's what I did

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

<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>
0 users liked answer #23dislike answer #230
Trail3lazer profile pic
Trail3lazer

Copyright © 2022 QueryThreads

All content on Query Threads is licensed under the Creative Commons Attribution-ShareAlike 3.0 license (CC BY-SA 3.0).