How can I close a dropdown on click outside? - javascript

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:
I do not feel really comfortable with the idea of having a global Subject that act as the connector between those components.
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:
tracking-click-events-outside-the-current-component
selectors-and-outputs-can-have-the-same-name
DirectiveMetadata
Host Binding

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

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

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.

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

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>

You can use mouseleave in your view like this
Test with angular 8 and work perfectly
<ul (mouseleave)="closeDropdown()"> </ul>

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

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.

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

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.

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!

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

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

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 second link item
A third link item
A fourth link item
<a
href="#"
class="list-group-item list-group-item-action disabled"
tabindex="-1"
aria-disabled="true"
>A disabled link item</a
>
</div>
</div>

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

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

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

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>

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

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.

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

Super complicated. I read them but not possible to reproduce them with my code.
I have this code for a dropdown menu in 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. But there are two missing functions.
When I click somewhere else, dropdown does not close.
When I click on the dropdown menu, go to an URL address.
Thanks

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>

You can use the (click) event on the document and check if the event target is not within the dropdown container. 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;
}
}

Related

How to detect click on children vue/nux custom directive

I struggle when I try Vue.js custom directive I try to create drop down with search inside. And I don't know how to detect when children clicked
<div v-dropdown-directive class="dropdown">
<div class="title">This is dropdown title</div>
<div class="close">Close dropdown</div>
</div>
Directive
import Vue from 'vue'
Vue.directive('dropdown-directive', {
bind (el) {
el.event = (e) => {
if (el === e.target) {
console.log('parent clicked')
el.classList.add('open') // this will add class open to parent, so dropdown will open
} else if (el.contains(e.target)) {
console.log('children clicked')
// this is working, but when I click on children has class "title" i'ts also working,
// I want only work when I click on children contain class "close"
} else if (!el.contains(e.target)) {
console.log('detect click outside')
el.classList.remove('open')
}
}
}
})
In the snippet below:
if you click on the red, it's the parent element
if you click on the blue-title, it's the title element
if you click on the blue-close, it's the close element
if you click outside the parent element, it's outside
Vue.directive('DropdownDirective', {
bind(el, binding) {
let type = binding.arg;
const clickFunction = (e) => {
if (e.target.classList.contains('close')) {
console.log('close')
} else if (e.target.classList.contains('title')) {
console.log('title:', e.target.textContent)
} else if (e.target === el) {
console.log('parent')
} else {
console.log('outside')
}
}
// adding a general eventListener, so you can check for clicks outside
window.addEventListener(type, clickFunction);
}
})
new Vue({
el: "#app"
})
.dd-item {
width: 50%;
background: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-dropdown-directive:click class="dropdown" style="background-color:red;">
<div class="title dd-item">This is dropdown title</div>
<div class="close dd-item">Close dropdown</div>
</div>
</div>
The point is:
you have to define the event you want to listen for. In the snippet I defined it as an argument of the directive (:click), and use that argument in the directive binding
you have to add the listener to that event (as you would have to with any JS code that should listen to an event)
you have to define the callback function of that eventListener
More on Vue directives: https://v2.vuejs.org/v2/guide/custom-directive.html

Simple way to detect click outside of child component

I want to expand and contract child div component depending on the value of a variable, but I want to be able to click out of that component (in the parent of the sibling) as well as collapsing it.
Here's an stackblitz example. I have attempted to use HostListener from what I found in this question, but it did not help my case.
#HostListener('document:click', ['$event'])
documentClick(event: MouseEvent) {
// your click logic
}
Objectives:
When I click on the child (hello) component, I want it to expand if it is not already and contract if it is already expanded
When I click on anything else (ie. parent or sibling component), I want the child (hello) component to contract if it is expanded.
I do not want the child (hello) component to expand when clicking in the parent/sibling.
Update: Using HostListener
hello.component.html
<div class="box" (click)="clicked()" [ngClass]="{large: isEnlarged}">Hello.component</div>
hello.component.ts
export class HelloComponent {
isEnlarged = false;
clicked() {
this.isEnlarged = !this.isEnlarged;
}
#HostListener('document:click', ['$event'])
documentClick(event: MouseEvent) {
console.log('clicked');
this.isEnlarged = false;
}
}
app.component
export class AppComponent {
}
The problem is your click handler is setting expanded to true before document click event handler sets it to false, so it's always false.
You could only set it to false if the event target is not your component:
https://stackblitz.com/edit/mouse-click-anywhere-8bwg6p?file=src/app/hello.component.ts
#HostListener('document:click', ['$event'])
documentClick(event: MouseEvent) {
console.log('clicked');
console.log(event);
if (event.target.id !== 'box') {
this.isEnlarged = false;
}
}

How to fix problem closing dropdown when I click outside it

I am developing an angular 5 project. My home page is composed by many components. In navbarComponent I have a dropdown list.
When the dropdown list is open, on clicking outside it, I would like it to close automatically.
This is my code:
ngOnInit() {
this.showMenu = false;
}
toggle() {
this.showMenu = !this.showMenu;
}
<div *ngIf="isConnect" class=" userStyle dropdown-toggle " (click)="toggle()">
<ul class="dropdown-menu subMenu" role="menu" *ngIf="showMenu">
<li (click)="profile()" class="subMenuItem"> PROFILE</li>
<li (click)="administration()" class="subMenuItem subMenuItem-last">ADMINISTRATION</li>
<li class="subMenuItem subMenuItem-last"><button class="btn blue-btn" (click)="logout()" ><mat-icon mat-list-icon fontIcon="icon-logout"></mat-icon>LOGOUT</button></li>
</ul>
</div>
This is how I implemented it in my project. First we need to bind click event on window in ngOnInit hook.
ngOnInit() : void {
this.windowClickSubscription = Observable
.fromEvent(window, "click")
.subscribe(this.handleWindowClick)
}
Now whenever there is a click on window our this.handleWindowClick will be called, lets add implementation of this method.
handleWindowClick(res: any) {
let target: any = res.target;
let threshold: number = 0;
while(target && target.className != 'grouped-control' && threshold <= 4) {
target = target.parentElement;
threshold++;
}
if(target && target.className != 'grouped-control') this.hasOptions = false;
}
This function will search for parent of event target until it finds grouped-control which we need to close when there is a click on window excluding this element. So if we fount that element we do nothing else we close it using hasOptions flag.
Finally we need to unbind that event on ngDestroy
ngOnDestroy(): void {
this.windowClickSubscription && this.windowClickSubscription.unsubscribe();
}
Now ofcourse you need to define this.windowClickSubscription property in your component and bind reference of component to function handleWindowClick in your constructor
Edit
To bind reference of constructor add the following line in your constructor
constructor() {
this.handleWindowClick = this.handleWindowClick.bind(this);
}
This will allow you to pass this function as callback handler and it will be executed with reference of your component.
Since we can show hide html with help of *ngIf I am toggling my control which I need to hide using flag this. hasOptions
When you open Dropdown adds any class like 'open' with class 'dropdown-toggle' and when you close dropdown removes that class. If you click outside the dropdown area it will close the dropdown.
I have achieved using below code
<div class="drop-menu">
<a class="dropdown-toggle" title="Filter" (click)="openDropdown()">
<span class="fa fa-arrow"></span>
</a>
<ul class="dropdown-menu subMenu" role="menu" *ngIf="showMenu">
<li (click)="profile()" class="subMenuItem"> PROFILE</li>
<li (click)="administration()" class="subMenuItem subMenuItem-last">ADMINISTRATION</li>
<li class="subMenuItem subMenuItem-last"><button class="btn blue-btn" (click)="logout()" ><mat-icon mat-list-icon fontIcon="icon-logout"></mat-icon>LOGOUT</button></li>
</ul>
</div>
Code for component.ts file:
constructor(private renderer: Renderer2) { }
ngOnInit() {
const selectDOM = document.getElementsByClassName('dropdown-toggle')[0];
this.renderer.listen('document', 'click', (evt) => {
const eventPath = evt.path;
const hasClass = _.where(eventPath, { className: 'drop-menu' });
if (hasClass.length <= 0) {
this.renderer.removeClass(selectDOM, 'open');
}
});
}
openDropdown() {
const selectDOM = document.getElementsByClassName('dropdown-toggle')[0];
if (selectDOM.classList.contains('open')) {
this.renderer.removeClass(selectDOM, 'open');
} else {
this.renderer.addClass(selectDOM, 'open');
}
}
Add a TemplateRef-Id to your menu:
<ul #menuRef class="dropdown-menu subMenu" role="menu" *ngIf="showMenu">
....
</ul>
Get that TemplateRef in Code:
#ViewChild('menuRef') menuRef: TemplateRef<any>;
Then you have to register a global (on document level) click event:
#HostListener('document:click', ['$event'])
hideMenu(event) {
if (!this.menuRef.nativeElement.Contains(event.target) {
if (this.showMenu) {
this.showMenu = false;
}
}
}
If the click was outside of your dropdown, you set showMenu=false and your menu closes.
But why not use a component for your dropdown? ng-select does all of that automatically.

How to remove element when clicking outside the element in javascript?

I have created a dropdown menu for my web app and when clicking on a icon i'm opening my dropdown menu. I want to remove the dropdown menu when i click anywhere other than the dropdown menu. My current approach removes the element when clicking outside the element. But i cannot open the dropdown menu when clicking on the icon after that. How can i fix it?
class DropDown extends Component {
constructor(props) {
super(props);
this.myFunction = this.myFunction.bind(this);
this.state = {
openmenu:false
}
};
myFunction(e) {
e.stopPropagation();
this.setState({
openmenu: !this.state.openmenu
})
render() {
window.onclick = function (event) {
if (!event.target.matches('myDropdown')) {
document.getElementById('myDropdown2').remove();
}
}
return (
<div className="dropdown small_font" id="myDropdown" >
<i className="fa fa-cog user_settings_icon" style={{marginTop: '6px'}} onClick={this.myFunction}
aria-hidden="true"></i>
{this.state.openmenu?
<div className="dropdown-content" id="myDropdown2">
<a className="dropdown_item"><i className="fa fa-trash-o margin_right10px" aria-hidden="true"></i>Delete</a>
<a className="dropdown_item"><i className="fa fa-flag-o margin_right10px" aria-hidden="true"></i>Report</a>
</div>:null
}
</div>
);
}
}
The error i'm getting when clicking on the icon for the second time
Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
If you don't want to keep track of adding and removing click events on unmount and a solution that works across all browsers id recommend using a library. Ive used https://github.com/Pomax/react-onclickoutside and it works very well, heres a snippet.
import React, { Component } from "react";
import onClickOutside from "react-onclickoutside";
class MyComponent extends Component {
handleClickOutside = evt => {
// ..handling code goes here...
};
}
export default onClickOutside(MyComponent);
If you dont want to use a library use the onBlur
class MyComponent extends Component {
handleClickOutside = evt => {
// ..handling code goes here...
};
render(){
<div onBlur={this.handleClickOutside}>
<SomeChild />
</div>
}
}
Lastly your using React wrong, your using it as if it was jquery which it is not. you dont remove anything manually. You have state that you update when to close the dropdown and when to open it.
Using events such as window.onclick in render method is not a good practice since every time you update your state this event is going to be instantiated.
I also noticed you have created the state prop openmenu and myFunction but you not using them properly.
My suggestion is to attach DOM events in ReactJS's lifecycle events:
componentDidMount() {
window.addEventListener('onClick', this.myFunction)
}
componentWillUnmount() {
// Make sure to remove such event when your component is unmounted
window.removeEventListener('onClick', this.myFunction)
}

Pass event through to Aurelia custom element

I have a Call To Action (CTA) which has a ripple effect, provided by a custom element called click-ripple, similar to Google Material Design. The custom element called click-ripple has a rule in the CSS to prevent this element from being clickable:
pointer-events: none;
If I do not add this rule, the element will be on top of its parent and it will not link the user through to the correct page or it will not perform the right action. Is there a way to feed an event from the parent through to one of its children without too much hassle?
EDIT
Let's say there is a button made up of a anchor-tag on the page. The markup of that anchor tag would look like this:
<template>
<a href="https://www.google.com">
<click-ripple></click-ripple>
</a>
</template>
My question is: what is an efficient way to feed a click action from the anchor tag forward to the click-ripple element?
The answer is to add an event listener to the parent of the current element. Looking it up in the W3 specification I came to the conclusion that I need to use element.parentElement.
clickripple.js
//Shortened for everyone's sanity
export class ClickRipple {
constructor(CssAnimator, Element) {
this.animator = animator;
this.element = element;
}
attached() {
this.element.parentElement.addEventListener("click", (event) => {
this.playAnimation(event);
});
}
playAnimation(event) {
//Math based on event
//Animation with Aurelia's CssAnimator
}
}
A common way of adding behavior (such as a click ripple) to an element is to use a custom attribute: <parent click-ripple></parent>.
Otherwise here's how you could pass the events to an arbitrary element:
click-through-custom-attribute.js
import {inject} from 'aurelia-framework';
#inject(Element)
export class ClickThroughCustomAttribute {
constructor(element) {
this.element = element;
this.handler = event => this.value.dispatchEvent(event);
}
attached() {
this.element.addEventListener('click', this.handler);
}
detached() {
this.element.removeEventListener('click', this.handler);
}
}
<require from="click-through-custom-attribute"></require>
<button ref="button1>I'm Behind</button>
<button click-through.bind="button1">I'm In Front</button>
If you know that the element you want to pass the click to is always going to be the parent element you could do something like this inside of your click-ripple custom element:
import {inject} from 'aurelia-framework';
#inject(Element)
export class ClickRipple {
constructor(element) {
this.element = element;
this.handler = event => this.element.parentElement.dispatchEvent(event);
}
attached() {
this.element.addEventListener('click', this.handler);
... add click ripple behavior ...
}
detached() {
... remove click ripple behavior ...
this.element.removeEventListener('click', this.handler);
}
}

Categories

Resources