How to fix problem closing dropdown when I click outside it - javascript

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.

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

Bootstrap4 scroll spy - active to parent li

I've got bootstrap4 menu like this:
<ul class="navbar-nav ml-auto">
<li class="nav-item"><a class="nav-link" href="#introduction">INTRODUKTION <span class="sr-only">(current)</span></a></li>
</ul>
Default scroll spy adds active to nav-link (a) I need to change this, becouse my active should be after nav-item (li). Can I do that ?
You can see this here:
Example
When I click, everything goes ok - but on scroll - active is a href.
By default .active class will be added to only anchor tags.
Try something like this for your requirement
$('[data-spy="scroll"]').on('activate.bs.scrollspy', function () {
$(".navbar-nav .active").removeClass("active").parent().addClass("active");
})
Add attribute data-spy="scroll"
on <div class="container"> the parent of section with id="introduction"
like
<div class="container" data-spy="scroll">
I found solution. I need just to add new event (cssClassChanged) - and working !
(function(){
// Your base, I'm in it!
var originalAddClassMethod = jQuery.fn.addClass;
jQuery.fn.addClass = function(){
// Execute the original method.
var result = originalAddClassMethod.apply( this, arguments );
// trigger a custom event
jQuery(this).trigger('cssClassChanged');
// return the original result
return result;
}
})();
and then
$(".nav-link").bind('cssClassChanged' , function(e) {
$(".nav-item").each( function() {
if( $(this).hasClass("active") == true ) {
$(this).removeClass("active");
}
});
$(this).removeClass("active").parent().addClass("active");
});

Navigation menu dropdown top level doesn't work

I have a dropdown menu with top level and two sub levels. The thing is that the sub levels work just fine, I can click on them and it takes me to the page I selected. The problem is with the top level, when I hover over it it displays the submenus but when I click on it it doesn't take me to the page.
var menu_Sub = $(".menu-has-sub");
var menu_Sub_Li;
$(".mobile-device .menu-has-sub").find(".fa:first").removeClass("fa-angle-right").addClass("fa-angle-down");
menu_Sub.click(function() {
if ($(".header").hasClass("mobile-device")) {
menu_Sub_Li = $(this).parent("li:first");
if (menu_Sub_Li.hasClass("menu-opened")) {
menu_Sub_Li.find(".sub-dropdown:first").slideUp(function() {
menu_Sub_Li.removeClass("menu-opened");
menu_Sub_Li.find(".menu-has-sub").find(".fa:first").removeClass("fa-angle-up").addClass("fa-angle-down");
});
} else {
$(this).find(".fa:first").removeClass("fa-angle-down").addClass("fa-angle-up");
menu_Sub_Li.addClass("menu-opened");
menu_Sub_Li.find(".sub-dropdown:first").slideDown();
}
return false;
} else {
return false;
}
});
menu_Sub_Li = menu_Sub.parent("li");
menu_Sub_Li.hover(function() {
if (!($(".header").hasClass("mobile-device"))) {
$(this).find(".sub-dropdown:first").stop(true, true).fadeIn("fast");
}
}, function() {
if (!($(".header").hasClass("mobile-device"))) {
$(this).find(".sub-dropdown:first").stop(true, true).delay(100).fadeOut("fast");
}
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="nav-menu">
<ul class="nav-menu-inner">
<li>
Home
</li>
<li>
<a class="menu-has-sub" href="about-us">About us <i class="fa fa-angle-down"></i></a>
<!-- Dropdown -->
<ul class="sub-dropdown dropdown">
<li>
<a class="menu-has-sub" href="clients-case-studies">Clients and Case Studies</a>
</li>
</ul>
<!-- End Dropdown -->
Any help would be appreciated. Thank you.
The problem arises in your return false call at the end of your first if statement:
menu_Sub.click(function () {
if ($(".header").hasClass("mobile-device")) {
menu_Sub_Li = $(this).parent("li:first");
if (menu_Sub_Li.hasClass("menu-opened")) {
...
}
else {
return false; // this prevents the default click action from occuring
}
});
What you are saying here is basically, if I click on the .menu-has-sub link and it doesn't have a .mobile-device class, I want it to return false.
That essentially means event.preventDefault() - read this SO answer for a great explanation event.preventDefault() vs. return false
But that seems to be your problem, be careful when preventing the default action on links, if you want them to go somewhere.
Here is a fiddle with the line commented out.

Trying to have class removed on dropdown after clicking anywhere else on the page in knockout.js

I'm adding a class after an event in knockout, and i can't seem to figure out how to get the class off of the div when clicking on the rest of the page. Here's my code. I tried a few things to no avail. I want to remove the "open" class on .nav-menu.cart when you click anywhere else on the page. Thanks
self.addProductToCart = function(data) {
var $productNotification = $(".product-notification");
ax.Cart.addCartItem({product_id:data.id, name:data.name, description:data.description});
$productNotification.slideDown(1000).fadeOut(200);
$(".nav-menu.cart").addClass("open");
};
Whenever you need to alter the DOM, you should try to do so via a knockout data-bind. Most of it can be done through knockout's default bindings; some stuff will require a custom binding.
For the first part: adding an open class should happen through the css bind:
data-bind="css: { 'classname' : state }"
In your viewmodel, add an cartIsOpen observable and an openCart method.
self.isOpen = ko.observable(false);
Now, you can open your cart visually by adding this data-bind to its container element:
data-bind="css: {'open': isOpen }"
The jquery line can be replaced by:
self.isOpen(true);
and knockout will take care of the css.
Now, for the (more complicated) second part:
Listening to an event outside a container is not something knockout's regular click or event binding can do. So you'll have to write a custom binding.
Listening to events outside a certain element can be quite tricky; I'd suggest searching stack overflow for some general advice. You'll have to be really careful with event.stopPropagation() for it to work.
In the example below I've coded a crude example of a clickOutside binding. Note that it's not a "finished" example:
Any click in your page will trigger the closeOnClickOutside method; you need to find a way to attach the listener when isOpen is set to true, and dispose it once it's set to false.
Whenever you want to open your menu you'll need to explicitly stop the click event from reaching the document. (cancelBubble in the example). Otherwise, the menu is opened and closed instantly.
var elementContainsChild = function(parent, child) {
// http://stackoverflow.com/a/2234986/3297291
var node = child.parentNode;
while (node !== null) {
if (node === parent) {
return true;
}
node = node.parentNode;
}
return false;
};
ko.bindingHandlers.clickOutside = {
init: function(element, valueAccessor) {
var cb = valueAccessor();
var closeOnClickOutside = function(e) {
if (e.target !== element &&
!elementContainsChild(element, e.target)) {
cb();
}
};
document.addEventListener("click", closeOnClickOutside);
}
}
var vm = {
isOpen: ko.observable(false)
};
ko.applyBindings(vm);
html, body { height: 100%; background: grey; }
.menu { background: yellow; display: none; }
.menu.open { display: block; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<button data-bind="click: isOpen.bind(null, true), clickBubble: false">open menu</button>
<div class="menu" data-bind="css: {'open' : isOpen }, clickOutside: isOpen.bind(null, false)">
<ul>
<li>Menu item</li>
<li>Menu item</li>
<li>Menu item</li>
<li>Menu item</li>
</ul>
</div>

How can I close a dropdown on click outside?

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

Categories

Resources