0

How can I add an eventlistener on a <div> or other element, to hide something I am displaying via an *ngIf - using Angular, when I click away from that element?

Explanation: I am showing a custom CSS dropdown via *ngIf when you click on <label>Filter</label>, and I want the user to be able to click as many times as they wish in the custom dropdown, but when they click outside the custom dropdown, I would like to hide the custom dropdown via the *ngIf again.

The method called when a user clicks on the label is showHideSectionOptions(), which toggles the showHide variable to true or false.

This is my HTML code:

showHide = false;

<div class="form-row">
  <div class="form-group" id="showAndHideSections">
    <label (click)="showHideSectionOptions()">
      <img src="../../../assets/icons/Filter.png" alt="" class="mr-3">Filter</label>
    <div *ngIf="showHide" class="section-options">
       // show or hide content
    </div>
  </div>
</div>

This is my component code:

showHideSectionOptions() {
  this.showHide = !this.showHide;
}

I have tried adding an eventlistener as per the below, but I cannot set the value of my showHide variable, as I get the following error: Property 'showHide' does not exist on type 'HTMLElement'.ts(2339):

body.addEventListener('click', function() {
  alert('wrapper');
}, false);
except.addEventListener('click', function(ev) {
  alert('except');
  ev.stopPropagation();
}, false);

Thanks in advance!

onmyway
  • 1,435
  • 3
  • 29
  • 53

1 Answers1

1

First of all, this already has an answer here

However, if you want an Angular solution, you can use a custom directive:

@Directive({
  selector: '[clickOutside]'
})
export class ClickOutsideDirective {
  @Output()
  readonly clickOutside = new EventEmitter<MouseEvent>();

  @Input()
  include?: HTMLElement;

  constructor(private el: ElementRef<HTMLElement>) {}

  @HostListener('window:click', [ '$event' ])
  onClick(event: MouseEvent): void {
    if (this.isEventOutside(event)) {
      this.clickOutside.emit(this.event);
    }
  }

  private isEventOutside(event: MouseEvent): boolean {
    const target = event.target as HTMLElement;

    return !this.el.nativeElement.contains(target) &&
        (!this.include || !this.include.contains(target))
  }
}

Which you can use like this:

<div class="form-group" id="showAndHideSections">
  <label (click)="showHideSectionOptions()" #label>
    <img src="../../../assets/icons/Filter.png" alt="" class="mr-3">
    Filter
  </label>
  <div *ngIf="showHide" class="section-options"
       [include]="label" (clickOutside)="showHide = false">
     // show or hide content
  </div>
</div>

A more performant one would be one running outside of the ngZone. Because the subscribe happens outside of the directive it will be inside the ngZone when subscribing to the Output

@Directive({
  selector: '[clickOutside]'
})
export class ClickOutsideDirective {
  @Input()
  include?: HTMLElement;

  @Output()
  readonly clickOutside = this.nz.runOutsideAngular(
    () => fromEvent(window, 'click').pipe(
      filter((event: MouseEvent) => this.isEventOutside(event))
    )
  );

  constructor(private el: ElementRef<HTMLElement>, private nz: NgZone) {}

  private isEventOutside(event: MouseEvent): boolean {
    const target = event.target as HTMLElement;

    return !this.el.nativeElement.contains(target) &&
        (!this.include || !this.include.contains(target))
  }
}

working stack

Poul Kruijt
  • 69,713
  • 12
  • 145
  • 149
  • Thank you very much for your answer. I really appreciate it! I am indeed looking for an Angular method of doing this. I have one problem with your solution; it is opposite of what I need. I have tried for 2 hours to fix it, but I am not coming right. I am sorry, I am not on your level yet :( I would like the element that is displayed with *ngIf="showHide" to be the ONLY element you CAN click on. – onmyway Aug 26 '20 at 13:51
  • @onmyway so you also don't want it toggle when the use clicks on `filter`? I've updated my stack to reflect this. If you agree upon it, I will also update the answer :) – Poul Kruijt Aug 26 '20 at 14:01
  • Yes you can drop the toggle. I like your stack! Just what i need :) – onmyway Aug 26 '20 at 14:30