Detecting clicks on a component is easy in Angular 2: Just use the (click)
event binding, pass a function from the component’s instance and you are done. However, if you need to detect clicks outside of your component, things are getting tricky. You will usually need this for custom implementations of drop-down lists, context menus, pop-ups or widgets.
As this is a functionality which you might use more often, you should wrap it in a reusable directive. Angular 2 offers a syntactically nice way to implement such a directive. So let’s go ahead and implement a simple click-outside directive in Angular 2!
tl;dr: Implementing this directive is super easy. If you feel lazy, just run npm i angular2-click-outside
.
Short Recap: Directives
In Angular 2, there are three types of directives: Components (element directives with templates), structural directives (adding or removing DOM elements) and attribute directives (modifying DOM element behavior or appearance). Here, we want to alter the behavior of a DOM element, which is why we need to use the latter one.
As the name suggests, we will define an attribute selector in order to resolve the directive. Later on, we’d like to do the following:
<ul class="contextMenu" (clickOutside)="close()"></ul>
Thus, choosing clickOutside
as our attribute selector seems to be reasonable. Attribute selectors are delimited by square brackets:
import {Directive} from '@angular/core'; @Directive({ selector: '[clickOutside]' }) export class ClickOutsideDirective { }
ElementRef
As we want to check if a click was performed on the current element, we need a reference to the DOM element on which the clickOutside
directive was placed on. In Angular 2, you can get this reference by injecting an ElementRef in the directive’s constructor. The framework will handle the rest.
import {Directive, ElementRef} from '@angular/core'; @Directive({ selector: '[clickOutside]' }) export class ClickOutsideDirective { constructor(private _elementRef : ElementRef) { } }
By using the private
access modifier in the constructor’s argument list, a property is automatically created for this variable with the same name.
Our algorithm for detecting outside clicks will be simple: We’ll capture all clicks on any element of the document and check if it was performed inside our own element which we now have a reference to. If the click wasn’t done inside our own element, the click must have been made outside of it. Simple logic.
Be careful: The documentation of ElementRef
states that using this API is the last resort when accessing DOM elements. This is perfectly true, as templating or data binding will help you in the most cases. In addition, those methods also work when using server-side rendering or other renderers. Here, the use case of checking clicks on certain DOM elements is highly renderer-specific (browser rendering only), so ElementRef
is okay to use in this particular case.
Event Binding
Next, we want to implement the event binding. As you might guess, this is really easy in Angular 2: All we need is a property of the EventEmitter
type. There’s also a generic version of this class, which takes a type parameter for the values it will emit. In most cases, the information that any element was clicked outside should be enough. Hence, we won’t emit any value here and thus use the non-generic version of EventEmitter
. It will be invoked every time a click was made outside.
In order to make our EventEmitter
visible as a event binding, we need to add the Output
annotation to the property. This annotation takes an optional argument which allows specifying a different attribute name for this event binding. If you don’t pass an argument, the property’s name will be used instead.
Remember: We want to use something like (clickOutside)="close()"
on the target element. By choosing the exact same name for both directive and event binding, we achieve this very compact syntax. As a result, we will use the exact same name of the selector also for the property holding the EventEmitter
instance:
import {Directive, ElementRef, Output, EventEmitter} from '@angular/core'; @Directive({ selector: '[clickOutside]' }) export class ClickOutsideDirective { constructor(private _elementRef : ElementRef) { } @Output() public clickOutside = new EventEmitter(); }
HostListener
We now have an event binding which can be invoked. Now let’s close the cycle by implementing the core of this directive.
The HostListener annotation of Angular 2 allows us to listen for certain events on the host, i.e. the DOM element. By using the document:
notation, you can also listen for events on document level, which is exactly what we want to do here: Capture all clicks on any element of the document and check if it is outside of our own element.
In addition, you can optionally define which values should be passed to the decorated method. The event parameter is a MouseEvent
, which holds the target element in the target
property. As we are only interested in this element, let’s simply inject this element as a parameter:
import {Directive, ElementRef, Output, EventEmitter, HostListener} from '@angular/core'; @Directive({ selector: '[clickOutside]' }) export class ClickOutsideDirective { constructor(private _elementRef : ElementRef) { } @Output() public clickOutside = new EventEmitter(); @HostListener('document:click', ['$event.target']) public onClick(targetElement) { } }
The onClick
method will now be invoked every time a click was performed on the whole document. Due to the use of HostListener
, you don’t even need to unbind from the event—Angular is handling everything for you.
Complete sample
Now let’s check if the element which our directive was placed on contains the target element which was clicked. This is a simple task using the contains
method from the DOM API. This method returns true
if the given node is a descendant of or equal to another node.
Here, we will use the DOM element which the directive was placed on: this._elementRef.nativeElement
. If the click was not placed inside the target element, it must have been placed outside. In this case, we will emit the clickOutside
event.
import {Directive, ElementRef, Output, EventEmitter, HostListener} from '@angular/core'; @Directive({ selector: '[clickOutside]' }) export class ClickOutsideDirective { constructor(private _elementRef : ElementRef) { } @Output() public clickOutside = new EventEmitter(); @HostListener('document:click', ['$event.target']) public onClick(targetElement) { const clickedInside = this._elementRef.nativeElement.contains(targetElement); if (!clickedInside) { this.clickOutside.emit(null); } } }
That’s all.
npm Module
I packaged this directive in an npm module called angular2-click-outside
. This snippet can be extended heavily for sure, so if you have suggestions for improvement, open an issue or file a pull request over at GitHub.
Thanks to my colleague Thomas Hilzendegen for reviewing this blog post. The Angular 2 logo licensed under the CC BY-SA 3.0 Unported License. The Angular 2 artwork is licensed under the CC BY 4.0 Unported License.