Have you ever needed a custom drop down menu where it’s dropdown state could be toggled based on click events? Maybe you’ve already developed a working solution using native DOM functionality or a library such as jQuery. If not, no problem!
Our goal is to write some code that will allow us to execute a callback function when we click “elsewhere” or outside of a target element’s descendant elements. We want our functionality to be reusable amongst multiple target elements such as custom dropdown menus. Oh, and we’re using Angular 4.
In summary, we want to use an Attribute Directive on our target element that sets up a Host Listener that listens for a click Mouse Event on the Document and emits the Mouse Event if the element being clicked is not a descendant of our target element. Our template containing the target element will bind a callback function to this mouse event and close the dropdown menu.
That was a bit of a mouthful. Let’s break it down.
What is an Attribute Directive? According to the official Angular docs at the time of this writing, Attribute Directives change the appearance or behavior of an element, component, or another directive.
So, like the definition states, we will be changing the behavior of our target element. We want our target element to emit an event when there is a mouse click outside of any of its descendant elements. Let’s take a look at our directive definition before we add any functionality to it.
import { Directive, ElementRef } from '@angular/core';
@Directive({ selector: '[clickElsewhere]' })
export class ClickElsewhereDirective {
constructor(private elementRef: ElementRef) {}
}
Using the @Directive decorator we can decorate a class as an Angular Directive. Notice the selector value: ‘[clickElsewhere]’. Since we want to be able to use this directive as an attribute on our target element, we are using square brackets which will select the attribute named ‘clickElsewhere’.
We also inject a reference to the target element that this directive is attached to using ElementRef. The reference to the target element is possible via the normal Angular dependency injection pipeline.
Let’s add a few more things to our directive:
import { Directive, EventEmitter, ElementRef, HostListener, Output } from '@angular/core';
@Directive({ selector: '[clickElsewhere]' })
export class ClickElsewhereDirective {
@Output() clickElsewhere = new EventEmitter();
constructor(private elementRef: ElementRef) {}
@HostListener('document:click', ['$event'])
public onDocumentClick(event: MouseEvent): void {
const targetElement = event.target as HTMLElement;
// Check if the click was outside the element
if (targetElement && !this.elementRef.nativeElement.contains(targetElement)) {
this.clickElsewhere.emit(event);
}
}
}
As you can see, we’ve imported a few more things from the @angular/core package that we need to complete this implementation: EventEmitter, HostListener and Output.
We’ve created a property called clickElsewhere and decorated it with the @Output() decorator to let Angular know this is an output property. This property is an EventEmitter and based on its type parameter: MouseEvent, it will emit mouse events. Event Emitters allow us to emit events up to our containing components. Notice the name of the output property we’ve created has the exact same as the selector for our directive: clickElsewhere. This allows us to use a very concise syntax when using this directive in a template, which we will see later.
Finally, we’ve defined an onDocumentClick function and decorated it with the @HostListener decorator. The Host Listener allow us to listen to an event on the host – which is the target element that this directive will be attached to. The first parameter of the @HostListener decorator is the event name. The second parameter is an array of arguments that are emitted in the event. Since we want to listen to click events on the whole document and not just the directive’s host, we went a step further and added document as our target before the click event name: document:click.
The @HostListener decorator also passes the arguments, $event in our case, into the function it’s decorating. So, our function will have one parameter: event, which will be of type MouseEvent.
Our function is pretty simple, it checks to see if the element being clicked on is NOT contained within the element that our directive is attached to. If the user clicked elsewhere or outside of the element that our directive is attached to, then we emit the mouse event as an output. To achieve this, we are taking advantage of the EventEmitter to emit the mouse event as our Output property.
Now that we have our directive setup we are ready to use it in a template. We would like to bind a callback function to the event that gets emitted–when the user clicks outside of the element we are attaching our directive to. I’ve set up a simple example that calls an openDropdown() function when the user clicks on a target div. When the user clicks elsewhere, a closeDropdown() function is called.
It’s as simple as that! You can see the concise syntax that I mentioned earlier:(clickElsewhere)=”closeDropdown()”. We are doing two things at once here. The first thing we’re doing is adding an attribute to the div called clickElsewhere which allows angular to find our directive by a selector: @Directive({ selector: ‘[clickElsewhere]’ }). The second thing we’re doing is binding the closeDropdown() callback function to the @Output() property in our directive that is also called clickElsewhere. Since the directive’s selector and output property both share the same name we are able to do this with a nice short syntax.
If our directive’s selector and output property had different names, we would have to declare the selector attribute on the target element, and bind to the output property separately. So, if we were to rename the output property to clickOff, we would have to use the directive like this:
If you are writing a directive where you plan to have multiple output events that you’d like to bind to, then the second syntax is going to be required. If you are writing a simple directive that does one thing, like we are in this example, you can leverage this concise syntax and have the template be a bit more readable.
I hope by taking the time to read through this example, you learned something new about the Angular pipeline!
We love to make cool things with cool people. Have a project you’d like to collaborate on? Let’s chat!
Stay up to date on what BizStream is doing and keep in the loop on the latest in marketing & technology.