Simple way to detect click outside of child component - javascript

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

Related

prevent onclick event while changing radio button of primeng

Hi have simple radio buttons of primeng, while changing the choise, I have some logic on the onClick function, checking if the user changed some fields, if he changed I will show message if he sure he want to leave the choice of the radio button, if he will press "cancel" I want to cancel all the event of the onlick function and to undo to his last choise. but the event of the onclick not doing it, I checked all the function of java script. I tried now to add HostListener that if some boolean field(the one that said the user want to undo)it will stopImmediatePropagation. but on runtime the onclick function called and not the HostListener. some ideas what to do?
radio button
<p-radioButton name="treesDetailsType" [(ngModel)]="selectedType" formControlName="selectedType" (onClick)="onChangeType(type,$event)" class="treeDetails" value="{{type.id}}" label="{{type.desc}}" [disabled]="isReadOnly && type.id != data.selectedType"></p-radioButton>
the onclick function
onChangeType(type, event) {
let change = this.checkChanges(type, event);
if (change) {
//HERE I WANT TO CANCEL ALL THE CHANGE AND TO LEAVE THE FUNCTION
this.clickDisallowed = true;
}
else {
switch (type.id)
.....
}
}
the host listner
#HostListener('click', ['$event']) onClick(event) {
if (this.clickDisallowed) {
event.stopImmediatePropagation();
}
console.log(event);
}
Maybe you are using the hostlistener wrongly.
In the angular example it is used inside a directive
https://angular.io/api/core/HostListener
Try this example!
#Directive({selector: 'button[counting]'})
class CountClicks {
numberOfClicks = 0;
#HostListener('click', ['$event.target'])
onClick(btn) {
console.log('button', btn, 'number of clicks:', this.numberOfClicks++);
}
}
#Component({
selector: 'app',
template: '<button counting>Increment</button>',
})
class App {}

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

Angular2 remove click event binding after first click

In my application I have a button with a click even on it:
<button class="btn btn-default" (click)="doSomething()">
From within the doSomething method, is there any way to remove the (click) event from the button (so the user can't trigger the functionnality anymore?).
I tried to set a disabled prop on the button but it doesn't change Angular2 behavior.
I tryed to use (click)="doSomething($event) and then
doSomething($event) {
// My method logic goes here
...
...
console.log('Method Logic');
//Attempt to overwrite click event
let target = event.target || event.srcElement || event.currentTarget;
this.renderer.listen(target, 'click', (event) => {
console.log('clic block');
});
}
But It doesn't "replace" the click event. So after that, on click, both original logic and the "click block" console log are triggered!
Method 1:
You can set a boolean variable, so if the user calls the function, boolean value changes and the user will be able to call the function on click again but actually nothing will happen.
bool: boolean = true;
doSomething($event) {
if (this.bool) {
// My method logic goes here
...
...
console.log('Method Logic');
this.bool = false;
}
}
Method 2:
You can add a condition to your html component, if specified variable (in this case bool) is true, the function is going to be executed, but only once because the bool variable will be set to false and the click function will execute nothing (null) from now on.
bool: boolean = true;
doSomething($event) {
// My method logic goes here
...
...
console.log('Method Logic');
this.bool = false;
}
(click)="bool ? doSomething($event) : null"
The downside of just adding a guard variable for the execution is that the actual event listener is still in place and will trigger Angular's change detection when clicked, even though it doesn't do anything.
To actually remove the event listener, you have to add it via the component's Renderer. This will return a function that removes the event listener when called:
import {Component, AfterViewInit, Renderer, ViewChild, ElementRef} from '#angular/core';
#Component({
template: `<button #button>...</button>`
})
export class SampleComponent implements AfterViewInit {
#ViewChild('button') button: ElementRef;
private cancelClick: Function;
constructor(private renderer: Renderer) {}
ngAfterViewInit() {
this.cancelClick = this.renderer.listen(this.button.nativeElement, 'click',
($event: any) => this.handleClick($event));
}
handleClick($event: any) {
this.cancelClick();
// ...
}
}
If your goal is to just remove the event listener when the event is fired for the first time, this can be implemented as a plugin to Angular's event system. I added it as a part of my ng2-events utility library [source], which lets you now do the following:
<button (once.click)="handleClick($event)">...</button>
For people dealing with Observable / Subject for handling some events :
<button (click)="clickEvents$.next($event)">
class MyComponent {
clickEvents$ = new Subject<MouseEvent>();
firstClick$ = this.clickEvents.take(1); // Observable of first click
}

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

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