Detect click outside Angular component
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.
Related videos on Youtube
AMagyar
I really enjoy programming! MCVE - Learn how to create the environment to ask the question
Updated on July 13, 2022Comments
-
AMagyar almost 2 years
How can I detect clicks outside a component in Angular?
-
Günter Zöchbauer over 7 years
-
Mohammad Kermani over 2 yearsAlso this answer: stackoverflow.com/a/51152404/3176270
-
-
J. Frankenstein over 6 yearsThis 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 about 6 yearsdoes it work on a component that created dynamiclly via : const factory = this.resolver.resolveComponentFactory(MyComponent); const elem = this.vcr.createComponent(factory);
-
Miguel Lara over 5 yearsA nice article on this topic: christianliebel.com/2016/05/…
-
Vikas Kandari over 4 yearsThis works perfectly with ngif or dynamic updates as well
-
edoardo849 over 4 yearsI think that this one should become the accepted answer as it allows for many optimizations: like in this example
-
Gizrah about 4 yearsWhile I agree with the way you think, I'd suggest not stuffing all logic in a
tap
operator. Instead, useskipWhile(() => !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 almost 4 yearsthis is the prettiest solution i got on the internet
-
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 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 over 2 yearsIs 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 over 2 yearsMaybe you can help me out here - stackoverflow.com/questions/69886444/…
-
Vincente over 2 yearsNiece solution, but
stopPropagation
might affect logic outside of the component: google analytics, closing of another component, etc -
David over 2 yearsIs there a way for the HostListener to only be active when there are subscribers interested in the events?
-
Quatsch over 2 yearsUpdate: This package has been deprecated