Based on this Article https://medium.com/#Taha_Shashtari/an-easy-way-to-detect-clicks-outside-an-element-in-vue-1b51d43ff634
i implemented the same methodology of the directive for detecting outside element click, at first i had to change things as vue 2 directives have been changed in vue 3, but i got so far that:
When i click the Icon to Toggle the Box -> The box is shown
When i click outside the Box -> The box is toggled
The only thing that isn't working is when i click inside the box itself it gets toggled again, which isnt suppose to happen.
Code
Directive:
let handleOutsideClick;
const closable = {
beforeMount(el, binding, vnode) {
handleOutsideClick = (e) => {
e.stopPropagation();
const { handler, exclude } = binding.value;
let clickedOnExcludedEl = false;
exclude.forEach((id) => {
if (!clickedOnExcludedEl) {
const excludedEl = document.getElementById(id);
clickedOnExcludedEl = excludedEl.contains(e.target);
}
});
if (!el.contains(e.target) && !clickedOnExcludedEl) {
binding.instance[handler]();
}
};
document.addEventListener("click", handleOutsideClick);
document.addEventListener("touchstart", handleOutsideClick);
},
afterMount() {
document.removeEventListener("click", handleOutsideClick);
document.removeEventListener("touchstart", handleOutsideClick);
},
};
export default closable;
PS: I changed the usage of refs into IDs
CartIcon:
<template>
<div
id="checkoutBoxHandler"
ref="checkoutBoxHandler"
#click="showPopup = !showPopup"
class="cart-icon"
>
<font-awesome-icon icon="fa-solid fa-cart-shopping" />
<span id="cart-summary-item">{{ cartItemsCount }}</span>
<div
v-show="showPopup"
v-closable='{
exclude: ["checkoutBox","checkoutBoxHandler"],
handler: "onClose",
}'
id="checkoutBox"
>
<CheckOutBox v-if="this.userCart" :userCart="this.userCart"></CheckOutBox>
</div>
</div>
</template>
onClose handler:
onClose() {
this.showPopup = false;
},
Can anyone see what i might be doing wrong here or maybe missing?
Thanks in advance
EDIT after Turtle Answers:
This is the Code i m using:
Directive:
const clickedOutsideDirective = {
mounted(element, binding) {
const clickEventHandler = (event) => {
event.stopPropagation();
console.log(element.contains(event.target))//True on click on the box
if (!element.contains(event.target)) {
binding.value(event)
}
}
element.__clickedOutsideHandler__ = clickEventHandler
document.addEventListener("click", clickEventHandler)
},
unmounted(element) {
document.removeEventListener("click", element.__clickedOutsideHandler__)
},
}
export default clickedOutsideDirective
Component:
<div
id="checkoutBoxHandler"
ref="checkoutBoxHandler"
#click="showPopup = !showPopup"
v-closable='onClose'
class="cart-icon"
>
<font-awesome-icon icon="fa-solid fa-cart-shopping" />
<span id="cart-summary-item">{{ cartItemsCount }}</span>
<div
v-show="showPopup"
ref="checkoutBox"
id="checkoutBox"
>
<CheckOutBox :userCart="this.userCart"></CheckOutBox>
</div>
</div>
The box is being displayed but on click on the box it still disappear
It looks like the problem could be multiple registered event listeners.
afterMount should be unmounted. If fixing that isn't enough, you may need to ensure you're unregistering the event correctly. You can store the handler on the element like this:
const closable = {
beforeMount(el, binding, vnode) {
el.__handleOutsideClick__ = (e) => {
e.stopPropagation();
const { handler, exclude } = binding.value;
let clickedOnExcludedEl = false;
exclude.forEach((id) => {
if (!clickedOnExcludedEl) {
const excludedEl = document.getElementById(id);
clickedOnExcludedEl = excludedEl.contains(e.target);
}
});
if (!el.contains(e.target) && !clickedOnExcludedEl) {
binding.instance[handler]();
}
};
document.addEventListener("click", el.__handleOutsideClick__);
document.addEventListener("touchstart", el.__handleOutsideClick__);
},
// The correct lifecycle method is 'unmounted'
unmounted(el) {
document.removeEventListener("click", el.__handleOutsideClick__);
document.removeEventListener("touchstart", el.__handleOutsideClick__);
},
};
export default closable;
Other advice
Don't call stopPropagation on the event, because it could swallow clicks on other UI elements.
Forward the event when invoking the handler so that the handler can inspect it.
To ensure your directive doesn't break, you probably don't want to reference the excluded nodes by ID, but rather by ref as in the article you linked.
Or, drop the exclusions feature altogether. Without it, your directive can look like below. It looks like you're only using it to exclude things that are already inside your popup. In my experience, clicked outside should mean clicked outside. If there are additional considerations, I would prefer to let the handler take care of them by inspecting the returned event.
import { Directive } from 'vue'
// Trigger a function when a click is registered outside the element
const clickedOutsideDirective = {
mounted(element, binding) {
const clickEventHandler = (event) => {
if (!element.contains(event.target)) {
binding.value(event)
}
}
element.__clickedOutsideHandler__ = clickEventHandler
document.addEventListener("click", clickEventHandler)
},
unmounted(element) {
document.removeEventListener("click", element.__clickedOutsideHandler__)
},
}
export default clickedOutsideDirective
Now the usage looks like this
<template>
<div
id="checkoutBoxHandler"
ref="checkoutBoxHandler"
#click="showPopup = !showPopup"
class="cart-icon"
>
<font-awesome-icon icon="fa-solid fa-cart-shopping" />
<span id="cart-summary-item">{{ cartItemsCount }}</span>
<div
v-show="showPopup"
v-clicked-outside='onClose'
id="checkoutBox"
>
<CheckOutBox v-if="this.userCart" :userCart="this.userCart"></CheckOutBox>
</div>
</div>
</template>
For me the best solution for this problem is to create some object in the background.
position:fixed;
top:0;
left:0;
width: 100vw;
height: 100vh;
z-index: check which value fits you here.
So at beginning before showing "box" that object do not exist. On box show, you also show that object which is in background, above all elements except your "box".
So only thing you can click outside of your "box" is that object. And you can put event on "that object click".
And on box hide, you also hide that object;
I know this can be done with jQuery as shown here: How to have click event ONLY fire on parent DIV, not children?
$('.foobar').on('click', function(e) {
if (e.target !== this)
return;
alert( 'clicked the foobar' );
});
But in angular, this keyword is not binded to the element when I use something like:
<div ng-dblclick="ctrl.doubleClickHandler($event)">
parent, event should fire here.
<div>child, event should not fire here.</div>
</div>
and in my controller:
this.doubleClickHandler = doubleClickHandler;
function doubleClickHandler(event) {
console.log(this); // this binds to controller
console.log(event);
}
The event fires, but I need to prevent it from firing when I click on the element's children.
I don't want to hardcode a check on the event.target based on class or attribute because it might change later. Is there anyway to achieve this within the HTML tag, or in JavaScript without hardcoding the element's class or attributes (similar to the technique of binding the this keyword in jQuery)?
You can compare target and currentTarget. The former is the clicked element and the latter is the element with the handler.
function doubleClickHandler(event) {
if (event.target !== event.currentTarget) return;
// do something
}
I think you could try something like this:
<div ng-dblclick="ctrl.doubleClickHandler($event) $event.stopPropagation()">
reference: What's the best way to cancel event propagation between nested ng-click calls?
If your using jquery try this,
<div class="foobar"> .foobar (alert)
<span>child (no alert)</span>
</div>
$('.foobar').on('click', function(e) {
if (e.target !== this)
return;
$scope.myFunc(e);
});
$scope.myFunc = function(e) {
$scope.clicked = "Parent Cicked";
$scope.$apply();
};
DEMO
surely its not leading to a good programmatic skills. Hence you can try below example with angular and JS
<div class="foobar" data-parent="true" ng-click="myFunc($event)"> .foobar (alert)
<span>child (no alert)</span>
</div>
// add data attribute as data-parent="true"
$scope.myFunc = function(e) {
if (! e.target.hasAttribute('data-parent'))
return;
$scope.clicked = "Parent Cicked";
};
//if clicked element has `data-parent` property do the things.
DEMO
i update the answer by create directive with jquery in your sample.
var app = angular.module("app", []);
app.controller("controller", function ($scope) {
var ctrl = this;
ctrl.doubleClickHandler = function () {
alert("parent clicked")
}
});
app.directive("directiveName", function () {
return {
restrict: "A",
scope: {
click: "&"
},
link: function (scope, element, attr) {
element.on('click', function (e) {
if (e.target !== this) {
return;
} else {
scope.click()
}
});
}
}
})
.foobar span {
background: red;
}
<!DOCTYPE html>
<html ng-app="app" ng-controller="controller as ctrl">
<head>
<title></title>
</head>
<body>
<div click="ctrl.doubleClickHandler()" directive-name class="foobar">
parent
<span>
child
</span>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
</body>
</html>
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;
}
}