Reputation: 426
I'm writing a directive for close element when a user clicks outside of the host element. But I run into trouble because @HostListener
is starting to call immediately when element displayed in the DOM. Maybe the problem isn't with @HostListener
...
@angular/core
version is 6.1.3
browser
Google chrome 73.0
import {Directive, ElementRef, EventEmitter, HostListener, Output} from '@angular/core';
@Directive({
selector: '[appClickOutside]'
})
export class ClickOutsideDirective {
@Output() public appClickOutside = new EventEmitter();
constructor(private el: ElementRef) {
}
@HostListener('document:click', ['$event.target'])
public onClick(target: HTMLElement) {
const isClickedInside = this.el.nativeElement.contains(target);
if (!isClickedInside) {
return this.appClickOutside.emit(null);
}
}
}
@Component({
templateUrl: 'my-html-file.html'
})
export class Component {
isOpened: boolean = false;
open () {this.isOpened = true;}
close () {this.isOpened = false;}
}
<!-- my-html-file.html -->
<main class="main">
<div class="container">
<div class="content">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quo, voluptatum!</p>
<button (click)="open()">Opend my modal</button>
</div>
<div class="sidebar"></div>
</div>
</main>
<div class="modal" (appClickOutside)="close()">
<div class="modal-header">
<h2 class="modal-title">Hello world</h2>
<button class="modal-close" (click)="close()">X</button>
</div>
<div class="modal-body">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Maiores, quidem.</p>
</div>
</div>
The element should be in the DOM, but it appears and immediately disappears when I'm clicking on the button.
Upvotes: 0
Views: 2325
Reputation:
TL;DR: See Edit 2
Here is what happens:
open()
method of Component
class gets called.<div (appClickOutside)="close()"....>
element gets activated and rendered.document.click
event was registered due to previous click and still in progress, the onClick()
method inside the ClickOutsideDirective
gets called. isClickedInside
is evaluated as falsy
, since you know, the activated div
element wasn't the target element at this timeif (!isClickedInside)
then becomes truty
, thus the appClickOutside
event is emitted, which will cause the close()
callback to be called.Here is an example, that demonstrates what's going on (Please open your developer console to see the logs): https://stackblitz.com/edit/angular-nci8or
However, you can easily work around this problem by wrapping this.isOpened = true;
with setTimeout
, which looks like this:
setTimeout(() => {this.isOpened = true; });
P.S. If no delay time is specified, setTimeout()
is simply executed when the stack is unwound.
Edit: The title of your question actually sums up the trouble you are having pretty well, and I didn't address it directly, sorry about that.
Well, at least in the third step, you can guess what happens behind the scenes. A click event is triggered, and before it bubbles further upwards till the document
element, the <div>
element is activated and rendered. Meanwhile, the target element is the button element, but that click
event is also registered for document
element too - even it's not reached the document
element yet. The rest simply describes what happens next. Here is more about event bubbling and capturing: https://javascript.info/bubbling-and-capturing
Edit 2: The ideal solution would be the following:
Template: Pass the MouseEvent
to the method
<button (click)="open($event)">open modal</button>
Component: Prevent the event from bubbling up
open($event: MouseEvent) {
$event.stopPropagation();
this.isOpened = true;
}
Upvotes: 1
Reputation: 2831
You can use the ngez library which has an outside click directive: https://ngez-platform.firebaseapp.com/#/core/outside-click
<div
(ngezOutsideClick)="onOutsideClick()"
style="width: 200px;
height: 200px;
background-color: aquamarine">
</div>
Upvotes: 1
Reputation: 28738
When you click on the button it's already outside of the div with MODAL, so it triggers the directive and the div remains closed. To remedy that, put the directive on a wrapper div. It will contain the button and the modal div. Something like this :
<div style="border:1px solid red; width:100px" (appClickOutside)="close()">
<button (click)="open()">open modal </button>
<div style="position:relative;width:300px;border:1px solid black" *ngIf="isOpened">MODAL</div>
</div>
I added some styling to show the position and div limites.
demo.
Upvotes: 1