Angular custom directives

Bilkiss Dulloo
7 min readDec 31, 2020

--

Part 2: Structural directives

In this 2-part series, we are looking at how to implement custom directives in Angular. As a reminder, in the first part, we looked at how to create a custom attribute directive to implement a drag & drop file upload functionality.

In this second part, we are going to look at structural directives, and how to use them to dynamically change our DOM structure.

What are directives?

As we have discussed before, there are three kinds of directives in Angular:

  1. Components — directives with a template.
  2. Attribute directives — change the appearance or behavior of an element, component, or another directive, and
  3. Structural directives — change the DOM layout by adding and removing DOM elements.

Components are the most common of the three directives and are one of the core building blocks in the Angular framework to build applications. They are basically the same as an attribute directive except that their selector is actually a tag, and they have an associated template markup.

On the other hand, attribute directive, such as our previously visited FileUploadDirective, changes the appearance and/or behavior of the host DOM element according to the logic implemented.

Finally, structural directives, which we’ll be looking at today, can change our DOM structure, usually by adding, removing, or manipulating elements. As with other directives, structural directives are usually applied to a host element. The directive then does whatever it’s supposed to do with that host element and its descendants.

Structural directives are easy to recognize in html markup: the directive’s attribute name is always preceded by an asterisk (*).

<span *ngIf=”user” class=”name”>Welcome {{user.name}} !</span>

In the above example, the span is either present or absent from the dom, depending on whether the “user” property of our component class is set or not.

Structural directives are important as they let us control the DOM and how it reacts to changes in our underlying data. Now let’s see how to build our own.

How to create our structural directive

The process to create a structural directive is not very different from the one to create an attribute directive. Again, we can simply use the CLI to generate our class. For example:

ng g directive directives/myStructuralDirective

This will generate a new typescript class decorated with the @Directive decorator, which will contain our directive’s implementation logic.

So, what are we building today?

The structural directive we are building will allow us to conditionally attach or remove an element in our dom, depending on whether a logged in user has a specific role and as such the permission to see the element our directive will be attached to. Sounds hard? No worries, as usual we’re going to take it slow :)

Step 1. Generating the class file

Similarly to an attribute directive, a minimal structural directive would consist of a class annotated with the @Directive decorator, which specifies the selector that identifies our directive. This class will then implement the desired directive behavior.

So we begin by generating our directive class using the CLI

ng generate directive directives/enableForRole

This command will do three things:

  1. Generate our class file at ‘src/app/directives/enable-for-role.directive.ts’,
  2. Generate a corresponding test file at ‘src/app/directives/enable-for-role.directive.spec.ts’, and
  3. Declare the directive class in our root AppModule.

The generated main directive class enable-for-role.directive.ts will look as follows:

import { Directive} from ‘@angular/core’;@Directive({
selector: ‘[appEnableForRole]’
})
export class EnableForRoleDirective { constructor() {}}

Here we are importing the @Directive decorator, and using it to configure our directive class. The directive’s selector is defined by default as ‘[appEnableForRole]’, but let’s change that to ‘[enableForRole]’ for convenience and readability.

Again, similarly to attribute directives’ selectors, It’s the brackets ([]) that make it an attribute selector. Angular will locate each element in the template that has an attribute named appEnableForRole and apply the logic of this directive to that element.

As opposed to attribute directives though, when using our structural directives in our markup, we need to prefix its selector with an asterisk (*). This is how Angular can tell that this is a structural directive rather than just a regular old attribute directive. Below is an example of how we would use this particular structural directive in our markup:

<p *enableForRole=”admin”>This tag is only displayed for admin users!</p>

But let’s not get ahead of ourselves just yet. We still need to build the logic of our directive!

Step 2. Adding the necessary imports

After the @Directive decorator comes the directive’s class, called EnableForRoleDirective, which contains the (currently empty) logic for the directive. Exporting EnableForRoleDirective makes the directive accessible.

We will now edit the generated src/app/directives/enable-for-role.directive.ts file. First, we will import TemplateRef and ViewContainerRef from ‘@angular/core’, and inject them in our constructor. The resulting file shall look as follows:

import { Directive, TemplateRef, ViewContainerRef } from ‘@angular/core’;@Directive({
selector: ‘[enableForRole]’
})
export class EnableForRoleDirective {constructor(
private templateRef: TemplateRef<any>,
private viewContainerRef: ViewContainerRef
) {}
}

We have added two new imports to our directive class. What do they do?

Well let’s begin with TemplateRef.

TemplateRef represents the contents of an embedded <ng-template> that can be used to instantiate embedded views. In short, it will basically contain the innerHTML of our host element. In our case, this would be the content we want to add/remove depending on the user role.

Next is the ViewContainerRef. We will be using this class to either instantiate a new embedded view based on the contents our TemplateRef described above, or clear the content if the user doesn’t have the necessary role.

So, in a very simplified summary, our TemplateRef will contain the innerHTML structure of our directive’s host element, and our ViewContainerRef will enable us to either render this content or clear it.

Sidequest 1: More about <ng-template>

In the last step, we have seen that TemplateRef will reference the contents of an <ng-template> element. But what does that mean?

Ng-template is a virtual element in angular. What thi means is that tags within a pair of <ng-template> tags will not be rendered by default. This is useful if we need to control when to include / exclude specific blocks of markup from the rendered DOM.

But wait, we aren’t using any ng-template tags in our markup, so how will this work? Well, it is all thanks to the structural directive :) Remember how we said that we would use our new directive like the following?

<p *enableForRole=”admin”>This tag is only displayed for admin users!</p>

Well, because of that asterisk, this code will be transformed by angular into the following:

<ng-template [enableForRole]=”`admin`”>
<p>This tag is only displayed for admin users!</p>
</ng-template>

And our TemplateRef will therefore contain our <p> tag.

Step 3. Implementing the actual logic

We will now (finally) be writing the logic of our directive. So in short what we will be doing here is firstly, create a virtual property with the same name as our selector, to which we will be assigning our desired role from the html. We do this by creating a setter, which will run anytime we try to assign a value to the property, and decorating it with the @input decorator, so that it will have access to the role we are assigning from the html.

We will then get the user data from storage (presumably we will have saved it there after a sign in) and check whether the user has the role specified above. If they do, we will render the contents of the TemplateRef inside our ViewContainerRef. Else we will clear the ViewContainerRef. Here is what the completed code looks like:

Sidequest 2: Mocking our user data

In our directive, we are fetching the user object from the browser’s localStorage. Usually we will have saved it there when a user successfully logs in. For the sake of brevity in this demo however we will be directly mocking the data inside our storage.

In our browser’s developer tools console, we can inject data into the storage by using the following commands.

// Mocking an adminlocalStorage.setItem(“user”, JSON.stringify({
first_name: “Test”,
last_name: “Lastname”,
role: “admin”,
email: “admin@test.io”
}) );

And to overwrite the mocked data with a non admin user, we can run

// Mocking a normal userlocalStorage.setItem(“user”, JSON.stringify({
first_name: “Test”,
last_name: “Lastname”,
role: “user”,
email: “user@test.io”
} ));

And we now have our mock user data inside our storage.

Step 4. Apply the attribute directive

To use the new EnableForRoleDirective, We will add a paragraph (<p>) element to the template of our AppComponent and apply the directive as an attribute.

<p *enableForRole=”`admin`”>I am an Admin!</p>

Now we can run the application to see the EnableForRoleDirective in action.

ng serve

You will notice that depending on what user role is stored in our storage, The directive will insert or remove our <p> tag accordingly.

To summarize, Angular found the EnableForRole attribute on the host <p> element. It created an instance of the EnableForRoleDirective class and injected a reference to the <p> element into the directive’s constructor which will show or hide the <p> element depending on the user role.

So, in conclusion, a structural directive is not that different from an attribute directive. The main thing that differentiates the two variants is that the structural directives allow us to easily modify the dom structure by allowing us to access and manipulate the dom structure of our host elements’ DOM structure.

Also, by digging around, you may notice that the directive does not yet react to changes to the login status. We will look at how to use rxjs to make it a bit more reactive in a future article.

--

--

Bilkiss Dulloo

In the field of front end dev more than 10 years, I am passionate and thrilled that there are so much new and exciting technologie/frameworks to learn everyday.