haacki47
haacki47

Reputation: 426

Why Angular immediately invoke @HostListener when element is starting display in DOM?

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

Answers (3)

user6576367
user6576367

Reputation:

TL;DR: See Edit 2

Here is what happens:

  1. First the open() method of Component class gets called.
  2. Then the <div (appClickOutside)="close()"....> element gets activated and rendered.
  3. Immediately afterwards, since the 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 time
  4. if (!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

Christian
Christian

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

Vega
Vega

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

Related Questions