Detect click outside Angular component

166,243

Solution 1

import { Component, ElementRef, HostListener, Input } from '@angular/core';

@Component({
  selector: 'selector',
  template: `
    <div>
      {{text}}
    </div>
  `
})
export class AnotherComponent {
  public text: String;

  @HostListener('document:click', ['$event'])
  clickout(event) {
    if(this.eRef.nativeElement.contains(event.target)) {
      this.text = "clicked inside";
    } else {
      this.text = "clicked outside";
    }
  }

  constructor(private eRef: ElementRef) {
    this.text = 'no clicks yet';
  }
}

A working example - click here

Solution 2

An alternative to AMagyar's answer. This version works when you click on element that gets removed from the DOM with an ngIf.

http://plnkr.co/edit/4mrn4GjM95uvSbQtxrAS?p=preview

  private wasInside = false;
  
  @HostListener('click')
  clickInside() {
    this.text = "clicked inside";
    this.wasInside = true;
  }
  
  @HostListener('document:click')
  clickout() {
    if (!this.wasInside) {
      this.text = "clicked outside";
    }
    this.wasInside = false;
  }

Solution 3

Binding to document click through @Hostlistener is costly. It can and will have a visible performance impact if you overuse(for example, when building a custom dropdown component and you have multiple instances created in a form).

I suggest adding a @Hostlistener() to the document click event only once inside your main app component. The event should push the value of the clicked target element inside a public subject stored in a global utility service.

@Component({
  selector: 'app-root',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent {

  constructor(private utilitiesService: UtilitiesService) {}

  @HostListener('document:click', ['$event'])
  documentClick(event: any): void {
    this.utilitiesService.documentClickedTarget.next(event.target)
  }
}

@Injectable({ providedIn: 'root' })
export class UtilitiesService {
   documentClickedTarget: Subject<HTMLElement> = new Subject<HTMLElement>()
}

Whoever is interested for the clicked target element should subscribe to the public subject of our utilities service and unsubscribe when the component is destroyed.

export class AnotherComponent implements OnInit {

  @ViewChild('somePopup', { read: ElementRef, static: false }) somePopup: ElementRef

  constructor(private utilitiesService: UtilitiesService) { }

  ngOnInit() {
      this.utilitiesService.documentClickedTarget
           .subscribe(target => this.documentClickListener(target))
  }

  documentClickListener(target: any): void {
     if (this.somePopup.nativeElement.contains(target))
        // Clicked inside  
     else
        // Clicked outside
  }

Solution 4

Improving @J. Frankenstein answear

  
  @HostListener('click')
  clickInside($event) {
    this.text = "clicked inside";
    $event.stopPropagation();
  }
  
  @HostListener('document:click')
  clickOutside() {
      this.text = "clicked outside";
  }

Solution 5

Above mentioned answers are correct but what if you are doing a heavy process after losing the focus from the relevant component. For that, I came with a solution with two flags where the focus out event process will only take place when losing the focus from relevant component only.

isFocusInsideComponent = false;
isComponentClicked = false;

@HostListener('click')
clickInside() {
    this.isFocusInsideComponent = true;
    this.isComponentClicked = true;
}

@HostListener('document:click')
clickout() {
    if (!this.isFocusInsideComponent && this.isComponentClicked) {
        // do the heavy process

        this.isComponentClicked = false;
    }
    this.isFocusInsideComponent = false;
}

Hope this will help you. Correct me If have missed anything.

Share:
166,243

Related videos on Youtube

AMagyar
Author by

AMagyar

I really enjoy programming! MCVE - Learn how to create the environment to ask the question

Updated on July 13, 2022

Comments

  • AMagyar
    AMagyar almost 2 years

    How can I detect clicks outside a component in Angular?

  • J. Frankenstein
    J. Frankenstein over 6 years
    This doesn't work when there is an element controlled by an ngIf inside the trigger element, since the ngIf removing the element from the DOM happens before the click event: plnkr.co/edit/spctsLxkFCxNqLtfzE5q?p=preview
  • Avi Moraly
    Avi Moraly about 6 years
    does it work on a component that created dynamiclly via : const factory = this.resolver.resolveComponentFactory(MyComponent); const elem = this.vcr.createComponent(factory);
  • Miguel Lara
    Miguel Lara over 5 years
    A nice article on this topic: christianliebel.com/2016/05/…
  • Vikas Kandari
    Vikas Kandari over 4 years
    This works perfectly with ngif or dynamic updates as well
  • edoardo849
    edoardo849 over 4 years
    I think that this one should become the accepted answer as it allows for many optimizations: like in this example
  • Gizrah
    Gizrah about 4 years
    While I agree with the way you think, I'd suggest not stuffing all logic in a tap operator. Instead, use skipWhile(() => !this.isActive), switchMap(() => this._utilitiesService.documentClickedTarget), filter((target) => !this._elementRef.nativeElement.contains(target)), tap(() => this._toggleMenuSubject$.next(false)). This way you utilize way more of RxJs and skip some subscriptions.
  • Anup Bangale
    Anup Bangale almost 4 years
    this is the prettiest solution i got on the internet
  • ginalx
    ginalx almost 4 years
    @lampshade Correct. I talked about this. Read the answer again. I leave the unsubscribe implementation to your style (takeUntil(), Subscription.add()). Don't forget to unsubscribe!
  • Nilesh Kumar
    Nilesh Kumar almost 4 years
    @ginalx I implemented your solution, it works as expected. Though ran into an issue the way I use it. Here's the question, please take a look
  • LeonTheProfessional
    LeonTheProfessional over 2 years
    Is it guaranteed that clickInside will be called earlier than clickout in the case that the click was done on an element lying inside?
  • Tom Rudge
    Tom Rudge over 2 years
    Maybe you can help me out here - stackoverflow.com/questions/69886444/…
  • Vincente
    Vincente over 2 years
    Niece solution, but stopPropagation might affect logic outside of the component: google analytics, closing of another component, etc
  • David
    David over 2 years
    Is there a way for the HostListener to only be active when there are subscribers interested in the events?
  • Quatsch
    Quatsch over 2 years
    Update: This package has been deprecated