Detect click outside Angular component

HtmlEventsAngularTypescript

Html Problem Overview


How can I detect clicks outside a component in Angular?

Html Solutions


Solution 1 - Html

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 - Html

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 - Html

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 - Html

Improving @J. Frankenstein answear

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

Solution 5 - Html

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.

Solution 6 - Html

ginalx's answer should be set as the default one imo: this method allows for many optimizations.

The problem

Say that we have a list of items and on every item we want to include a menu that needs to be toggled. We include a toggle on a button that listens for a click event on itself (click)="toggle()", but we also want to toggle the menu whenever the user clicks outside of it. If the list of items grows and we attach a @HostListener('document:click') on every menu, then every menu loaded within the item will start listening for the click on the entire document, even when the menu is toggled off. Besides the obvious performance issues, this is unnecessary.

You can, for example, subscribe whenever the popup gets toggled via a click and start listening for "outside clicks" only then.


isActive: boolean = false;

// to prevent memory leaks and improve efficiency, the menu
// gets loaded only when the toggle gets clicked
private _toggleMenuSubject$: BehaviorSubject<boolean>;
private _toggleMenu$: Observable<boolean>;

private _toggleMenuSub: Subscription;
private _clickSub: Subscription = null;


constructor(
 ...
 private _utilitiesService: UtilitiesService,
 private _elementRef: ElementRef,
){
 ...
 this._toggleMenuSubject$ = new BehaviorSubject(false);
 this._toggleMenu$ = this._toggleMenuSubject$.asObservable();

}

ngOnInit() {
 this._toggleMenuSub = this._toggleMenu$.pipe(
      tap(isActive => {
        logger.debug('Label Menu is active', isActive)
        this.isActive = isActive;
        
        // subscribe to the click event only if the menu is Active
        // otherwise unsubscribe and save memory
        if(isActive === true){
          this._clickSub = this._utilitiesService.documentClickedTarget
           .subscribe(target => this._documentClickListener(target));
        }else if(isActive === false && this._clickSub !== null){
          this._clickSub.unsubscribe();
        }

      }),
      // other observable logic
      ...
      ).subscribe();
}

toggle() {
    this._toggleMenuSubject$.next(!this.isActive);
}

private _documentClickListener(targetElement: HTMLElement): void {
    const clickedInside = this._elementRef.nativeElement.contains(targetElement);
    if (!clickedInside) {
      this._toggleMenuSubject$.next(false);
    }    
 }

ngOnDestroy(){
 this._toggleMenuSub.unsubscribe();
}

And, in *.component.html:


<button (click)="toggle()">Toggle the menu</button>

Solution 7 - Html

You can useclickOutside() method from https://www.npmjs.com/package/ng-click-outside package

Solution 8 - Html

Alternative to MVP, you only need to watch for Event

@HostListener('focusout', ['$event'])
  protected onFocusOut(event: FocusEvent): void {
    console.log(
      'click away from component? :',
      event.currentTarget && event.relatedTarget
    );
  }

Solution 9 - Html

u can call event function like (focusout) or (blur) then u put your code

<div tabindex=0 (blur)="outsideClick()">raw data </div>
 

 outsideClick() {
  alert('put your condition here');
   }

Solution 10 - Html

another possible solution using event.stopPropagation()

  1. define a click listener on top most parent component which clears the click-inside variable
  2. define a click listener on the child component which first calls the event.stopPropagation() and then sets the click-inside variable

Solution 11 - Html

Solution

Get all parents

var paths       = event['path'] as Array<any>;

Checks if any parent is the component

var inComponent = false;	
paths.forEach(path => {
	if (path.tagName != undefined) {
		var tagName = path.tagName.toString().toLowerCase();
		if (tagName == 'app-component')
			inComponent = true;
	}
});

If you have the component as parent then click inside the component

if (inComponent) {
	console.log('clicked inside');
}else{
	console.log('clicked outside');
}

Complete method

@HostListener('document:click', ['$event'])
clickout(event: PointerEvent) {
	
	var paths       = event['path'] as Array<any>;
	
	var inComponent = false;	
	paths.forEach(path => {
		if (path.tagName != undefined) {
			var tagName = path.tagName.toString().toLowerCase();
			if (tagName == 'app-component')
				inComponent = true;
		}
	});
	
	if (inComponent) {
		console.log('clicked inside');
	}else{
		console.log('clicked outside');
	}
	
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAMagyarView Question on Stackoverflow
Solution 1 - HtmlAMagyarView Answer on Stackoverflow
Solution 2 - HtmlJ. FrankensteinView Answer on Stackoverflow
Solution 3 - HtmlginalxView Answer on Stackoverflow
Solution 4 - HtmlJohn LibesView Answer on Stackoverflow
Solution 5 - HtmlRishanthakumarView Answer on Stackoverflow
Solution 6 - Htmledoardo849View Answer on Stackoverflow
Solution 7 - HtmlJames BondView Answer on Stackoverflow
Solution 8 - HtmlMiguel Angel Romero HernandezView Answer on Stackoverflow
Solution 9 - HtmlRahul YadavView Answer on Stackoverflow
Solution 10 - HtmlHoseinGhanbariView Answer on Stackoverflow
Solution 11 - HtmlWictor ChavesView Answer on Stackoverflow