Explain Codes LogoExplain Codes Logo

Detect click outside Angular component

javascript
event-emitter
click-detection
performance-optimization
Anton ShumikhinbyAnton Shumikhin·Nov 8, 2024
TLDR

Instantly trap click events occurring outside an Angular component by deploying the @HostListener decorator to observe document clicks. To distinguish from inside clicks, a comparison is made between the clicked object and your component's element within your directive:

import { Directive, ElementRef, Output, EventEmitter, HostListener } from '@angular/core'; @Directive({ selector: '[appClickOutside]' }) export class ClickOutsideDirective { @Output() clickOutside = new EventEmitter<void>(); constructor(private elementRef: ElementRef) {} @HostListener('document:click', ['$event.target']) public onDocumentClick(targetElement: HTMLElement): void { const clickedInside = this.elementRef.nativeElement.contains(targetElement); if (!clickedInside) { this.clickOutside.emit(); // Yeah, nothing to see here; move along 👮‍♂️ } } }

Just attach this directive to any elements wishing to detect outside clicks:

<div appClickOutside (clickOutside)="onOutsideClick()"> <!-- Magical component content --> </div>

Give onOutsideClick() function in your component the mission to handle the fallout from an outside click.

Beam up performace with efficient unsubscription

While it's pure joy to track click events, don’t overlook your application's performance and possibly summon the memory leak monsters. Developers often go trigger-happy with listeners and don't pay heed to clean up. You wouldn’t want to leave your toys all over the park now, would you?

Always ensure you tidy up the listener when the directive goes out of service. Sounds like a knight’s code, alright!

@HostListener('document:click', ['$event.target']) public onDocumentClick(targetElement: HTMLElement): void { // rest of the code... ngOnDestroy() { this.clickOutside.complete(); // Time to call it a day, folks! 🌙 }

Calling complete() on the EventEmitter signifies that no more values will be emitted. It's downright OCD-worthy but an excellent and necessary practice for maintaining observable hygiene.

Be careful while using ngIf as it could deceptively remove an element harboring the clickOutside directive. This could result in your listener possessing stale state or obsolete references. It’s always the swift and silent ones, right?

Advanced detection techniques tackling tricky situations

Global utility service to the rescue

For those extra challenging cross-component click detection scenarios, you might need to call in your heavy-duty global service. It’s like your superhero friend who sweeps in to save the day, any day, any component!

@Injectable({ providedIn: 'root' }) export class ClickOutsideService { private _clickedOutsideSource = new BehaviorSubject<HTMLElement>(null); public clickedOutside$ = this._clickedOutsideSource.asObservable(); public emitClickedOutsideElement(target: HTMLElement): void { this._clickedOutsideSource.next(target); } }

Simply subscribe to clickedOutside$ and flexibly manage actions based on clicks in any component.

Lock and load with 'contains' method

This powerful method is the quick-draw gun-slinger in the Wild West of click detection. When you need to find out if a click was inside or outside, just go ahead and unleash this method on the component's native element:

// Alright, let’s see what you got const clickedInside = this.elementRef.nativeElement.contains(targetElement);

Run heavy-duty actions, conditionally!

In heavy duty situations where clicking outside triggers some performance-impacting actions, you better lay out some tripwires or conditional checks. These checks can save you from uncalled-for hefty operations (like starkly closing a dropdown) when the state doesn't change. Yeah, close that dropdown like it owes you money!

Flags hoisted high for clickout custom events

For managing component-specific focus, raise the flags high using focus events. Moreover, emit a custom 'clickout' event for achieving fine-tuned control over click actions. It’s like marking your territory in the click wilderness:

@HostListener('focus') onFocus() { this.hasFocus = true; // Okay folks, eyes on the prize! } @HostListener('document:click', ['$event.target']) public onDocumentClick(targetElement: HTMLElement): void { if (this.hasFocus && !this.elementRef.nativeElement.contains(targetElement)) { this.hasFocus = false; this.emitClickOutside(); // Looks like the ship just left the port ⛴️ } }

More than just detection - User experience level-up!

Handling stopPropagation

Sometimes, you tend to stop the event from frantically bubbling up to ancestor elements in nested components. It's like bringing a toddler's tantrum under control before it disturbs the entire supermarket!

onClick(event: Event): void { event.stopPropagation(); // Alright, take a deep breath and relax... 😌 }

Cushy UI interactions

A top-notch product requires a top-tier user interface! When dabbling with UI elements like dropdown menus and modals, an outside click detection becomes more of a magic wand of user experience design. Wave that wand and poof — round up a concrete and fluid user navigation.

Performance drift — Are we sinking?

While stocking up on all these useful features, remember, with great power comes great responsibility. Every addition — every line of code injected — leaves a footprint on performance. Always strive for that perfect mix of power and responsibility where features enhance user experience while maintaining a robust application performance.