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]
})
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>
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>
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.
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>
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>
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>
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';
}
}
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>
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;
}
}
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.
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