Angular Attribute Directives: A Practical Approach
You’re working on an Angular app trying to add an interactive UI element like a simple dropdown menu. You have twenty browser tabs open and you’re trying to learn the “Angular way” of doing it. You want to follow best practices, but your frustration level is rising and you find yourself thinking why is it so difficult to do something that would have taken 10 minutes with Jquery and Bootstrap. Sound familiar? If it does, you’ve come to the right place.
Don’t sweat it— you’ve got this! It’s actually the perfect use case for a custom attribute directive and I will cover everything you need to know in order to implement your own in minutes. Let’s dive in!
What is an attribute directive?
Attribute directives change the appearance or behavior of an element, component, or another directive. Essentially, it is a class annotated with the
Directive decorator where you specify what change you want to occur and what CSS event (if any) you want to trigger that change.
ngModel are examples of attribute directives built-in to the Angular framework.
When would I use an attribute directive?
You would use an attribute directive anytime you want logic or events to change the appearance or behavior of the view. Dropdowns, accordions, and tabs are just a few common use cases for custom attribute directives. When you have a UI element that will be common throughout your app, you can implement an attribute directive and share it across components and modules to avoid repeating the code for the same functionality.
Before I get into how attribute directives work, I need to explain a few things. In order for attribute directives to accomplish what we want them to accomplish, they need to be able to access and modify DOM elements. There are three ways to accomplish this in an Angular directive:
- HostBinding (This should be your default)
I will show you examples of a dropdown menu that uses each one of these methods as I explain how they work. The HTML and CSS in each example will be exactly the same. Only the code in the Dropdown class will change from example to example.
The dropdown directive will add or remove the
open class to the button element when the click event is emitted. The button class has a sibling div containing the dropdown links with a class of
dropdown. By default, the
display property of the dropdown div is set to none. When the dropdown directive applies the
open class to the dropdown button, the
display property of the dropdown div is set to
block further down in the cascade, making it visible. With this setup, adding and removing the
open class will be how we display or hide the dropdown list.
Attribute Directive using ElementRef
In this section, we will look at a dropdown directive implementation using ElementRef. You can find the example to which I will refer on Stackblitz here.
In this example, this is how our dropdown directive looks:
In the Gist above, we have an attribute directive, a
DropdownDirective class annotated with the
Directive decorator. Generally, decorators in Angular contain metadata the compiler needs to understand how a class should be processed, instantiated, and used at runtime. The only property required by a directive is the
So how do we use the dropdown directive?
First, we need to import it into our module by adding it to the declarations array. Then we use the dropdown directive by placing the value of the
selector property on the desired HTML element as if it were an attribute. In our case, it is
You can set the
selector property to whatever you want, but by convention, it is the name of your class prefixed with something relevant to your project. The reason for the prefix is to help avoid collisions with standard HTML attributes and other directives used by any 3rd-party libraries you may be importing.
How does this work?
When the template parser reaches the button element on which we placed our
appDropdown directive, the Angular compiler searches for a directive with a selector set to
appDropdown and instantiates the class associated with it: The
In order to add and remove the
open class and make our directive function, we need to access the button element on which we placed our directive. We can accomplish that by passing an object of type
ElementRef into the constructor of our directive. The Angular compiler understands that we want a reference to the host element injected into our directive and assigned to the
elRef property. In this example, the host element is the button element because it is the element on which our dropdown directive is placed.
HostListener decorator allows us to specify what CSS event we want to listen for on the host element (our button element) and the function we want to execute when that event is emitted. In this example, the
toggleDropdown function will be executed when a user clicks on the button element.
In terms of Jquery, think of the
HostListener as the event method (think
.click()) and the
toggleDropdown function as the callback you want to execute when the event is triggered.
You might be thinking The HostListener is weird. Can’t I just attach an event listener manually? The short answer is Not Safely. The
HostListener decorator solves some really important problems for you. In the Angular Docs, you will find the following excerpt:
1. You have to write the listeners correctly.
2. The code must detach the listener when the directive is destroyed to avoid memory leaks.
3. Talking to DOM API directly isn’t a best practice.
Now, let’s take a look at what is happening inside of the
toggleClass function. We get the DOM representation of the button element from the
nativeElement property the same as if we were to use a Jquery element selector. If the
classList DOM property contains the
isOpen is true and the
toggle method removes the
open class and closes the dropdown. Likewise, if the
open class is not found in the
isOpen is false and the
toggle method adds the
open class and opens the dropdown.
Remember when we we decided earlier that we should use the
HostListener decorator because it is not a good idea to access the DOM directly? Using
ElementRef to access the DOM and manipulate it is doing just that. Permitting direct access to the DOM like this can make your app more vulnerable to XSS attacks. For information, check out the Angular security guide.
In addition, this method of manipulating the DOM tightly couples the DOM and the rendering layer. This is problematic if you ever want to use web/service workers, as they cannot directly access the DOM. Luckily, there is an alternative method out there which leads me to the second way to implement the dropdown attribute directive.
Attribute Directive using Renderer2
The Renderer2 API offers a way to bypass Angular’s templating and make custom UI changes that can’t be expressed declaratively. That means that web and service works can safely use this method. You can find this example on Stackblitz here.
Let’s take a look at what our
dropdown.directive.ts file looks like now.
In the Gist above, we still inject the
ElementRef to get the reference to the host element; however, we inject the
Renderer2 in order to make changes to the view.
Like in the previous example, we get the
isOpen boolean value by checking to see if the
classList property contains the
open class. If true, the dropdown is open and we use the
removeClass method on the R
enderer2 API to remove the
open class and close the dropdown. Inversely, if
isOpen is false, we call the
addClass method on the
Renderer2 API to add the
open class and open the dropdown.
By using the
Renderer2 API, we intercept calls to the renderer and modify the template rather than making changes to the DOM directly. This decouples the rendering layers and the DOM, making the use of web/service workers possible.
As we discussed earlier, because we’re still injecting the
ElementRef there is still the the possibility that we are introducing a security vulnerability into our application. We can solve this problem with the
HostBinding decorator, which leads me to the third way to implement an attribute directive.
Attribute Directive with HostBinding
HostingBinding decorator allows us to mark a DOM property as a host-binding property. In other words, we can bind a DOM property on the host element to local property on our
DropdownDirective class by passing the DOM property into the
Hostbinding decorator. You can find this example on Stackblitz here.
Let’s take a look at our
dropdown.directive.ts file now.
In the Gist above, note that we’re not injecting anything into our
DropdownDirective class. This is because we don’t really need access to the element. We just need to bind the
isOpen property to whether or not the
open class is applied to the host element. We can do that by passing a CSS selector prefixed with
class. into the
HostBinding decorator. If the
open class is set on the host element, it will return true. Otherwise, it will return false.
Note: You can bind to as many properties on the host element as needed. In our case, we only need one.
toggleDropdown function, we can add/remove the
open class from the host element by setting the
isOpen property to the opposite boolean value. When the function annotated by the
HostListener decorator changes a property on the directive class bound to the DOM property, we do not have to manually change the it with an
ElementRef or the
Renderer. Instead, Angular automatically checks host property bindings during change detection, and if a binding changes it updates the host element of the directive using a version of the
The benefits of using this method include:
- Our DOM is decouple from the rendering layer
- Avoid vulnerability to XSS attacks by referencing the DOM directly
- Easier to test and improves the readability
Avoid accessing and manipulating the DOM directly. Instead, use the
HostListener decorators to avoid any vulnerabilities, decouple the rendering layer from the DOM, and improve testability.