How to close the drop-down list when clicking on the button outside?

I would like to close the login drop-down menu when the user clicks somewhere outside of this drop-down list, and I would like to do this with Angular2 and with Angular2 "...

I implemented the solution, but I'm really not sure about that. I think that there should be the easiest way to achieve the same result, so if you have any ideas ... let them discuss :)!

Here is my implementation:

Dropdown component:

This is the component for my dropdown:

  • Each time this component is set to visible (for example: when a user clicks on a button to display it), he subscribes to the rxjs "global" user interface stored within the SubjectsService.
  • And every time he is hidden, he unsubscribes from this topic.
  • Every click anywhere inside , the template of this component launches the onClick () method, which simply stops the event bubbles at 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; } } 

Application Component:

On the other hand, there is an application component (which is the parent of the drop-down list component):

  • This component catches every click event and emits 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 bothers me:

  • I don't really like the idea of ​​creating a global object that acts as a connector between these components.
  • setTimeout : this is necessary because this is what happens otherwise if the user clicks a button showing a drop-down list:
    • The user clicks a button (which is not part of the drop-down list component) to display the drop-down menu.
    • A drop-down list is displayed and subscribes immediately to the topic userMenu .
    • The event bubble is called before the application component and comes across
    • The application component emits an event in the userMenu theme
    • The dropdown component will catch this action on userMenu and hide the drop-down menu.
    • At the end, the drop-down list is never displayed.

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

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

+128
javascript drop-down-menu angular rxjs
Mar 01 '16 at 0:14
source share
20 answers

You can use the 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 a custom event as a directive. Check out these Ben Nadel posts:

+225
Mar 01 '16 at 2:07 on
source share

ELEGANT METHOD

I found this clickOut directive: https://github.com/chliebel/angular2-click-outside . I check this and it works well (I only clickOutside.directive.ts in my project). You can use it as follows:

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

Where close your function that will be called when the user clicks outside the div. This is a very elegant way to deal with the problem described in the question.

If you use the directive above to close the popup, be sure to first add event.stopPropagation() to the button click event handler that opens the popup.

BONUS:

Below I clickOutside.directive.ts the directive code from the file clickOutside.directive.ts (in case the link stops working in the future) - author 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); } } } 
+34
Mar 07 '17 at 8:35
source share

I did it like that.

An event listener has been added in the click document and in that the handler checked whether my container event.target contains, if not, hide the drop-down menu.

It will 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"; } } } 
+18
May 29 '16 at 11:12
source share

I think that Sasha accepted return work for most people. However, I had a situation where the contents of the element that should listen for events with a click changed dynamically. Thus, the element's nativeElement element does not contain event.target when it was created dynamically. I could solve this problem 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 the Ref element is in the path (DOM path to the target) of the event. Thus, it is possible to process dynamically created elements.

+16
Feb 26 '17 at 21:59
source share

If you do this on iOS, use the touchstart event:

As with Angular 4, HostListener decoration 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 answer if (!this.eRef.nativeElement.contains(event.target) { doSomethingCool(); } } } 
+11
Sep 04 '17 at 1:14 on
source share

We are working on a similar problem at work today, trying to figure out how to make the dropdown div disappear when called. Ours are slightly different from the original post question, because we did not want to click on another component or directive, but just outside of a specific div.

We ended his solution using an event handler (window: mouseup).

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

2.) In the internal drop-down menu (the only thing we wanted for the clicks to NOT close the menu), we added an event handler (window: mouseup) and passed it to $ event. NOTE. This cannot be done with the usual click handler, as this contradicts the parent click handler.

3.) In our controller, we created a method that we would like to name in the clickout event, and we use event.closest ( docs here ) to find out if the pressed spot is inside our target 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> 
+10
Jun 15 '16 at 17:52
source share

I did not make a workaround. I just attached a document: click on my switch function as shown below:

     @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 go out of my directive, I close the dropdown menu.

+5
Oct 30 '17 at 18:23
source share

From the drop-down list, you can create a sibling element that will cover the entire screen, which will be invisible, and be there only to capture click events. You can then detect clicks on this item and close the drop-down menu when it is clicked. Suppose that an element has a silkscreen of a class, here is the style for it:

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

The z-index should be high enough to place it above everything except the drop-down list. In this case, my dropdown will be b z-index 2.

Other answers worked in some cases for me, except that sometimes my drop-down menu would close when I interacted with elements inside it, and I didn’t want this. I added dynamically added elements that were not contained in my component, in accordance with the target event, as I expected. Instead of sorting this mess, I decided that I would just try to do it in a silkscreen way.

+4
Jan 12 '17 at 6:55
source share
 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; } } 

CONS: - Two event listeners for each of these components per page. Do not use this for components that are on the page hundreds of times.

+4
Jun 05 '17 at 15:43
source share

I would like to complement @Tony's answer as the event is not deleted after clicking outside the component. Full receipt:

  • Mark your main item 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); 
  • In an interactive element use:

     (click)="dropstatus=true" 

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

+3
Feb 18 '17 at 1:27
source share

The correct answer has a problem, if you have a clicakble component in your popover, this element will no longer be used for the contain method and will close based on @ JuHarm89, and 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
Dec 11 '17 at 17:25
source share

Best version for @Tony's 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"); } } } 

The css: // file is NOT required if you are using the bootstrap dropdown list.

 .ourDropdown{ display: none; } .ourDropdown.open{ display: inherit; } 
+2
Mar 15 '17 at 13:56 on
source share

You can write a 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 throws an event when the html element is in the DOM, and when the input property [clickOut] is "true". It listens for the mousedown event to handle the event before the item is removed from the DOM.

And one note: firefox does not contain the path property in the event, you can use the function to create the 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 the event handler in the directive: event.path should replace getEventPath (event)

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

+2
Jun 12 '17 at 19:17
source share

You have to check if you click on the modal overlay, much 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(); } } 
+2
Jul 31 '17 at 8:55
source share

If you use Bootstrap, you can do this directly using the bootstrap method via the drop-down menus (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 everything is fine to put the button (click)="clickButton()" on the button. http://getbootstrap.com/javascript/#dropdowns

+1
Nov 14 '16 at 10:57
source share

I also made a small workaround.

I created an event (dropdownOpen) , which I listen to in my element of the ng-select element and call a function that closes all the other SelectComponent, opened separately from the currently open SelectComponent.

I changed one function inside the select.ts file as shown below to fire an 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 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 for an event trigger inside a component that has an 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
Apr 04 '17 at 9:02 on
source share

NOTE: for those who want to use web workers, and you need to avoid using document and nativeElement, this will work.

I answered the same question here: stack overflow

Copy / Paste the link above:

I had the same problem when I was making a drop down menu and confirmation dialog, I wanted to reject them when clicking outside.

My final implementation works fine, but requires CSS3 animation and style.

NOTE : I have not tested the code below, there may be some syntax problems that need to be fixed, as well as obvious adjustments for your own project!

What I've done:

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

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 css3 that 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 don’t need anything visual, you can simply use such a transition

 .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
Nov 30 '17 at 10:55
source share

I made a directive to solve this similar problem and I am using Bootstrap. But in my case, instead of waiting for the click event outside the element to close the currently open dropdown menu, I think it's better if we follow the mouseleave event to automatically close the menu.

Here is 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> 
+1
Dec 25 '18 at 14:29
source share

MOST ELEGANT METHOD: D

There is one easiest way to do this, no instructions are needed for this.

"element-that-toggle-your-dropdown" should be a button tag. Use any method in the (blur) attribute. All this.

 <button class="element-that-toggle-your-dropdown" (blur)="isDropdownOpen = false" (click)="isDropdownOpen = !isDropdownOpen"> </button> 
0
May 29 '19 at 14:59
source share

I came across another solution inspired by focus / blur examples.

Thus, if you want to achieve the same functionality without connecting a global document listener, you can consider the following example valid. This also works on Safari and Firefox on OSx, although they have different button focus event handling: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus

Working example on the glass business 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(); } } 
0
Jul 30 '19 at 15:10
source share



All Articles