16

I upgraded my Angular from 4 to 6, and consequently had a problem with my click-off policy, it stopped working on all components.

my directive:

import { Directive, Output, EventEmitter, ElementRef, HostListener } from '@angular/core';
    
@Directive({
  selector: '[clickOutside]'
})
export class ClickOutsideDirective {
    
  constructor(private _elementRef : ElementRef) { }
    
  @Output()
  public clickOutside = new EventEmitter();
   
  @HostListener('document:click', ['$event.target'])
  public onClick(targetElement) {
    const clickedInside = this._elementRef.nativeElement.contains(targetElement);
    if (!clickedInside) {
        this.clickOutside.emit(null);
    }
  }
}

My component.html that makes use of this directive:

<div 
  id="sidenav" 
  *ngIf="this.opened" 
  class="sidenav" 
  [ngClass]="getClasses()" 
  [ngStyle]="getStyles()" 
  clickOutside 
  (clickOutside)="closeOutsideSidenav()"
>
  <header> {{ navTitle }} </header>
  <i 
    *ngIf="this.showCloseButton" 
    class="iconic iconic-x-thin close-icon" 
    (click)="closeSidenav()"
  ></i>
  <ng-content></ng-content>
</div>
<div 
  *ngIf="this.backdrop && this.opened" 
  class="sidenav-backdrop"
></div>
Maaddy
  • 618
  • 5
  • 13
André Montório
  • 311
  • 1
  • 4
  • 14

4 Answers4

20

view:

<div #insideElement></div>

component:

export class SomeClass {
  @ViewChild("insideElement") insideElement;
  @HostListener('document:click', ['$event.target'])

  public onClick(targetElement) {
    const clickedInside = this.insideElement.nativeElement.contains(targetElement);
    if (!clickedInside) {
      console.log('outside clicked');
    }
  }
}
ssuperczynski
  • 3,190
  • 3
  • 44
  • 61
YoungHyeong Ryu
  • 929
  • 7
  • 9
  • Unfortunately, this one does not support unsubscribing and continue working even when the component is destroyed. So, I would suggest to register event manually in ngOnInit and unsubscribe it in ngOnDestroy – Maxím G. Aug 29 '19 at 17:26
  • @MaximGeorgievskiy how would you accomplish that? I don't understand what the syntax would look like. sounds very different. could you make your own answer? – tatsu Jan 23 '20 at 14:05
  • @tatsu Good idea. I've added as a separate answer, with a link to a working demo. – Maxím G. Jan 26 '20 at 00:45
19

You're referencing "this" in your template, which is not necessary. I made a working example of that directive:

https://stackblitz.com/edit/angular-piqewb

And theres no reason to have the directive on the div twice.

<div id="sidenav" *ngIf="opened" class="sidenav" [ngClass]="getClasses()" [ngStyle]="getStyles()" (clickOutside)="closeOutsideSidenav()">
    <header> {{ navTitle }} </header>
    <i *ngIf="showCloseButton" class="iconic iconic-x-thin close-icon" (click)="closeSidenav()"></i>
    <ng-content></ng-content>
</div>

<div *ngIf="backdrop && opened" class="sidenav-backdrop"></div>
Pezetter
  • 2,783
  • 2
  • 22
  • 40
  • Thanks for the tips but it did not work. The "closeOutsideSidenav ()" method is not called – André Montório May 25 '18 at 14:40
  • Those aren't tips. Its a working example. Look at the stackblitz and ascertain what is different between your example and mine. – Pezetter May 25 '18 at 14:41
  • Your stackbittz code is not working for me :( @Everett – Ritwick Dey May 25 '18 at 15:54
  • @RitwickDey how so? – Pezetter May 25 '18 at 15:55
  • I tried to run into `NgZone`, then it is working. (I mean your code) – Ritwick Dey May 25 '18 at 16:01
  • Huh? Why would you need to run this in NgZone? Clicking outside of the highlight box displays "SHOW ME" – Pezetter May 25 '18 at 16:02
  • Yep. I was saying same. It is not working. I wrap it inside of Ngzone, it started to work – Ritwick Dey May 25 '18 at 16:12
  • No you were not saying the same. Like I said, clicking outside the highlight box works in that stackblitz. Your lack of explanation for that is a little sneaky. – Pezetter May 25 '18 at 16:15
  • Works great without any NgZone wrapping. – Koja Dec 18 '18 at 08:35
  • @Pezetter your stackblitz does not demonstrate a real-world scenario. maybe because it's angular 6. but if i add it to a basic angular 8 cli helloworld, it doesn't work. so something must be wrong. – tatsu Jan 09 '20 at 10:30
  • I ran into the same problem, but identified why: There is an `*ngIf` in my component which changes when I click inside my component, so angular first runs my method, changing the `*ngIf` condition, then deletes the element inside the `*ngIf`, then runs the "clickOutside" directive, where the clicked element is no more inside the component, so considers the click is outside... Has to add a `setTimeout(..., 0)` in my component method to be sure the clickOutside directive runs first. – Random Jan 09 '20 at 13:49
  • Thank you for the example in stackblitz. It worked. – Topman Jun 05 '20 at 01:28
11

This is a modification of @YoungHyeong Ryu answer above, but with unsubscription, so that the handler stops working when the component is unmounted.

DEMO https://stackblitz.com/edit/angular-1q4pga

import { Component, Input, OnInit, OnDestroy, ViewChild } from '@angular/core';

@Component({
  selector: 'app-click-outside',
  template: `<div #insideElement>Click outside me.</div>`
})
export class ClickOutsideComponent implements OnInit, OnDestroy  {
  @ViewChild('insideElement', { static: false }) insideElement;

  public ngOnInit() {
    this.onDocumentClick = this.onDocumentClick.bind(this);
    document.addEventListener('click', this.onDocumentClick);
  }

  public ngOnDestroy() {
    document.removeEventListener('click', this.onDocumentClick);
  }

  protected onDocumentClick(event: MouseEvent) {
    if (this.insideElement.nativeElement.contains(event.target)) {
      return;
    }
    console.log('Clicked outside!');
  }
}

Here, we remove the event listener on destroy. Also, normally a method added by addEventListener is executed in a global context (instead of this context); so we should take care of it and bind onDocumentClick method to this (we do it in ngOnInit). Now we can use this inside onDocumentClick.

Maxím G.
  • 860
  • 10
  • 14
  • wait isn't HostListener's subscription destroyed correctly by angular automatically? and `document.` is old syntax I'm pretty sure we can avoid calling document altogether in newer angular syntax. – tatsu Feb 03 '20 at 16:19
-2

Run the inside NgZone.

Example:

export class AppComponent {
  opened: boolean = false;

  constructor(private ngZone: NgZone) {
  }

  closeOutsideSidenav(e) {
    this.ngZone.run(() => {
      this.opened = !this.opened;
    })
  }
}

I added my code to stackblitz. https://stackblitz.com/edit/angular-gyhtym (click outside of the "Highlight Me!")

Ritwick Dey
  • 18,464
  • 3
  • 24
  • 37
  • "Running functions via run allows you to reenter Angular zone from a task that was executed outside of the Angular zone (typically started via runOutsideAngular)." This issue has nothing to do with Angular zone. https://angular.io/api/core/NgZone#run – Pezetter May 25 '18 at 16:11