Angular 2: A Simple Click Outside Directive

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.

Published by

Christian Liebel

Hey there! I am Christian Liebel from Leimersheim, Germany. I work as a consultant at Thinktecture and I am their representative at W3C. PWA development with Angular and .NET Core is our day-to-day business. Feel free to contact me anytime.