Custom Angular Directive Guide: Exploring Component, Attribute, and Structural Directives

Angular is a frontend framework based on components. Any project can be represented as a tree of components nested within each other.

In addition to components, we can also create directives in Angular. This raises the question: "What do components lack that makes directives necessary?"

In this post, you will learn what directives are in Angular, the different types of directives available, and how to create your own. Angular provides three types of directives: component directives, structural directives, and attribute directives.

Component directives

Funnily enough, you’re already familiar with the first type of directive, even if you think you’ve never used it. It’s called a component directive or simply a component.

When we create a component like this, it’s considered a directive:

@Component({
  selector: 'app-child',
  standalone: true,
  templateUrl: '<div>It is a child component</div>'
})
export class ChildComponent {}

We can then render it like this:

<app-child></app-child>

The child is a selector for our component. By rendering it, we encapsulate all logic inside, making it simple to reuse the component. However, remember that to use this component, you must import it as a dependency.

@Component({
  selector: 'app',
  ...
  imports: [ChildComponent]
})

child component

Yes, we call it a component, but essentially it is a component directive. To create it, we use a component decorator, where we can provide a template and styling. This is the main difference compared to other directives.

We use component directives when we need to encapsulate a piece of markup along with styling and some business logic. Typical examples of components include a header, sidebar, todo list, or filters.

Attribute directive

But sometimes, we don't want to create a component with markup and logic. We just want to make some changes to a DOM element, such as changing the background or applying some classes, for example.

When we talk about attribute directive, we define them like this:

<div foo>Here is some text</div>

In this case, foo is an attribute directive. It provides some behavior for the element where we apply this directive.

The most important difference is that attribute directive do not have a template, whereas component directives do.

When we create attribute directives, we simply apply some behavior or styles to an already existing DOM element.

In Angular, the most popular attribute directives are ngClass, ngStyle, and ngModel, which we use to change the behavior of an existing component.

ngClass

We use ngClass when we want to add classes conditionally.

<div [ngClass]="{active: isActive}">Here is some text</div>

ng class

Here, we add an active class when the isActive property is set to true. This approach allows us to combine classes without the need to concatenate strings, which can quickly become unreadable.

ngStyle

ngStyle allows us to apply styles conditionally without needing to define classes for them.

<div [ngStyle]="{background: isActive ? 'red' : 'blue'}">Here is some text</div>

ng style

This sets the background of the div to either red or blue, depending on the value of the isActive property.

ngModel

This directive enables two-way binding. When we update the property, it updates the value in the template. Similarly, when we change the input, it directly updates the bound property.

<input [(ngModel)]="name"/> {{name}}
<button (click)="changeName()">Change name</button>

Here, we use name as a property for ngModel. When we type in the input, the name property is updated in real time.

export class AppComponent {
  name = 'foo'
  changeName(): void {
    this.name = 'bar'
  }
}

When we call changeName, this value is changed, and our template is re-rendered.

ng-model

Structural directive

The last type we have is the structural directive. This is a directive that changes the structure of our DOM, which means it helps us add or remove elements from the DOM tree. We have several structural directives in Angular, such as ngIf, ngFor, and ngSwitch, and all of these directives start with an asterisk (*).

ngIf

ngIf allows us to remove or add a piece of markup to the template based on a specific condition.

<div *ngIf="isActive">Here is some text</div>

ng if

This div is rendered when our isActive property is set to true.

ngFor

ngFor allows us to iterate over a list of elements and render them in the template.

export class AppComponent {
  users = [
    {id: 1, title: 'foo'},
    {id: 2, title: 'bar'},
    {id: 3, title: 'baz'},
  ];
}

First, we define an array of data that we want to render in our component.

<div *ngFor="let user of users">{{user.title}}</div>

ng for

Here, we map through our users and gain access to all properties of each user.

ngSwitch

ngSwitch allows us to write a switch-case in the template.

<div [ngSwitch]="role">
  <div *ngSwitchCase="'admin'">Admin</div>
  <div *ngSwitchCase="'owner'">Owner</div>
  <div *ngSwitchDefault>User</div>
</div>

ng switch

Now, depending on the role, a different div is rendered.

Implementing custom attribute directive

Now you know that directives are needed, and you already use them a lot every single day. But often, it is not enough to use built-in directives to cover your needs. Luckily you can create custom directives in Angular.

That's when it's time to implement a custom attribute directive. Let's say we need to create a simple directive that will change the background of an element. Here’s how you would use it:

<div highlight>This is some text</div>

Let's create this custom directive.

// highlight.directive.ts
@Directive({
  selector: '[highlight]',
  standalone: true
})
export class HighlightDirective implements AfterViewInit {
  elementRef = inject(ElementRef)

  ngAfterViewInit() {
    console.log(this.elementRef)
  }
}

Here we defined our basic directive. First of all, we should not forget to write a selector in square brackets. Secondly, we inject ElementRef in the constructor to get access to the element where our directive is attached.

Now, we need to register it inside our app.module.ts.

@NgModule({
  declaration: [AppComponent, HighlightDirective]
})
export class AppModule {}

In our directive, we can directly access the DOM element and change it.

export class HighlightDirective implements AfterViewInit {
  elementRef = inject(ElementRef)

  ngAfterViewInit() {
    this.elementRef.nativeElement.style.background = 'yellow';
  }
}

highlight

This will change the background of the element where our directive is attached.

But typically, we are interested in some configuration from the outside, so let's make it possible to provide a background to our directive.

export class HighlightDirective implements AfterViewInit {
  @Input() color: string = 'yellow'

  elementRef = inject(ElementRef)

  ngAfterViewInit() {
    this.elementRef.nativeElement.style.background = this.color;
  }
}

As you can see, we provided an Input inside, and if it is not set, we get a default color. Now, let's use it.

<div highlight color="red">This is some text</div>

red

As you can see in the browser, our text has a red background because we changed it through the input.

But we can make it even more complex by changing the background only when we hover over the element.

export class HighlightDirective {
  @Input() color: string = 'yellow'

  elementRef = inject(ElementRef)

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.color);
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string) {
    this.elementRef.nativeElement.style.backgroundColor = color;
  }
}

In Angular, HostListener allows us to listen to events on the host element of the directive. Here, we removed ngAfterViewInit and added mouseenter and mouseleave instead. Now, we change the color only on hover and not always.

But what do we do if we want to change the background only when the user clicks the element? Here’s how it can be done.

export class HighlightDirective {
  @Input() color: string = 'yellow'

  elementRef = inject(ElementRef)

  @HostListener('click') 
  onClick() {
    this.elementRef.nativeElement.style.backgroundColor = this.color;
  }
}

ng style

We added a click handler by creating a HostListener, where we change the background inside.

Implementing custom structural directives

Now let's create a structural directive. It’s important to mention that I have never written a structural directive in the last 8 years. But if, for some reason, you need to do it, this is how it's done.

Here we want to create a directive like ngIf, but the opposite. It will be ngUnless.

@Directive({
  selector: '[unless]'
})
export class UnlessDirective {}

This is just an empty directive. Let's try to apply it to some element.

<div *unless="condition">This is some text</div>

<ng-template [unless]="condition">
  <div>This is some text</div>
</ng-template>

Here we added condition as a boolean in our app.component and used unless inside the ng-template. These are both the same usages. The usage with the asterisk is just syntactic sugar.

But actually, now we need to provide an input unless to our directive.

export class UnlessDirective {
  templateRef = inject(TemplateRef<unknown>)
  viewContainer = inject(ViewContainerRef)

  @Input() set unless(condition: boolean) {
    if (condition) {
      this.viewContainer.clear()
    } else {
      this.viewContainer.createEmbeddedView(this.templateRef)
    }
  }
}

To change our markup, we use TemplateRef and ViewContainerRef. TemplateRef represents an embedded template that can be used to create embedded views, while ViewContainerRef represents a container where we can attach our views.

Here, we not only get unless as an input, but we also perform some logic depending on the value we receive. If it's true, we remove the element. If it's false, we render the element using the createEmbeddedView function.

Now, if we set our condition property to false, we will see the element on the screen.

unless

As you can see in the browser, our two elements are rendered when we set condition to false.

Structural Directive vs Attribute Directives

Structural Directives

  • They change the DOM structure.
  • They have an asterisk (*) prefix.
  • They sometimes have templates or change the structure of the DOM.
  • Examples: ngIf, ngFor, ngSwitch.

Attribute Directives

  • They change the behavior or appearance of the component, element, or another directive.
  • They don't change the DOM layout.
  • Examples: ngModel, ngStyle, ngClass.

Real Use Cases for Custom Directive

If it all sounds dry to you, then it is time to look at real scenarios of how people use custom directives in Angular so you can get a better understanding of when it makes sense to use them.

Permissions Directive

I used a custom directive to show/hide certain elements based on the permissions that a user has. For example, you can see a menu for user management only if you have user management permissions, or you can see a button to create new users.

The directive does the necessary checking to determine whether the current user has the given permissions. The only input to this directive is the permission required to see the element. The directive itself pulls in the required services to check that.

Auto-placing Components

Custom directives to auto-place components (dropdown menus, popover menus) based on viewport/container width.

Modifying Appearance

Custom directives that monitors the window size and can modify the appearance of the page using CSS classes based on the page's width.

Resizing Textarea

Custom directives that resizes a textarea based on the amount of text inside.

Scroll into View

A custom directive that scrolls a component into view based on the current URL.

Converter

A custom directive that automatically converts a string to a number depending on the given pattern (int, +int, -int, float, etc.).

Autofocus

A custom directive that provides autofocus on input fields.

Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!

📚 Source code of what we've done
Did you like my post? Share it with friends!
Don't miss a thing!
Follow me on Youtube, Twitter or Instagram.
Oleksandr Kocherhin
Oleksandr Kocherhin is a full-stack developer with a passion for learning and sharing knowledge on Monsterlessons Academy and on his YouTube channel. With around 15 years of programming experience and nearly 9 years of teaching, he has a deep understanding of both disciplines. He believes in learning by doing, a philosophy that is reflected in every course he teaches. He loves exploring new web and mobile technologies, and his courses are designed to give students an edge in the fast-moving tech industry.