directive click outside angular 6
Asked Answered
F

4

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>
Fineness answered 25/5, 2018 at 14:15 Comment(2)
Does it help if you use the directive only once? Remove the clickOutside and use only (clickOutside)=...)?Footrace
I tried did not workUhhuh
C
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>
Caylor answered 25/5, 2018 at 14:27 Comment(12)
Thanks for the tips but it did not work. The "closeOutsideSidenav ()" method is not calledUhhuh
Those aren't tips. Its a working example. Look at the stackblitz and ascertain what is different between your example and mine.Caylor
Your stackbittz code is not working for me :( @EverettDich
@RitwickDey how so?Caylor
I tried to run into NgZone, then it is working. (I mean your code)Dich
Huh? Why would you need to run this in NgZone? Clicking outside of the highlight box displays "SHOW ME"Caylor
Yep. I was saying same. It is not working. I wrap it inside of Ngzone, it started to workDich
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.Caylor
Works great without any NgZone wrapping.Dalliance
@Caylor 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.Hunan
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.Bumf
Thank you for the example in stackblitz. It worked.Unaneled
L
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');
    }
  }
}
Lydie answered 28/1, 2019 at 7:5 Comment(3)
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 ngOnDestroyParaphrastic
@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?Hunan
@Hunan Good idea. I've added as a separate answer, with a link to a working demo.Paraphrastic
C
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>
Caylor answered 25/5, 2018 at 14:27 Comment(12)
Thanks for the tips but it did not work. The "closeOutsideSidenav ()" method is not calledUhhuh
Those aren't tips. Its a working example. Look at the stackblitz and ascertain what is different between your example and mine.Caylor
Your stackbittz code is not working for me :( @EverettDich
@RitwickDey how so?Caylor
I tried to run into NgZone, then it is working. (I mean your code)Dich
Huh? Why would you need to run this in NgZone? Clicking outside of the highlight box displays "SHOW ME"Caylor
Yep. I was saying same. It is not working. I wrap it inside of Ngzone, it started to workDich
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.Caylor
Works great without any NgZone wrapping.Dalliance
@Caylor 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.Hunan
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.Bumf
Thank you for the example in stackblitz. It worked.Unaneled
R
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.

Ransome answered 26/1, 2020 at 0:44 Comment(1)
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.Hunan
D
-1

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!")

Dich answered 25/5, 2018 at 15:59 Comment(1)
"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. angular.io/api/core/NgZone#runCaylor

© 2022 - 2024 — McMap. All rights reserved.