In version 15, Angular introduced a new directives composition API that allows developers to compose existing directives into new more complex directives or components. This allows us to encapsulate behaviors into smaller directives and reuse them across the application more easily. In this article, we will explore the new API, and see how we can use it in our own components.
All the examples from this article (and more) can be found on Stackblitz here.
Starting point
In the article, I will use two simple directives as an example
HighlightDirective
- a directive borrowed from Angular's Getting started guide. This directive will change an element's background color whenever the element hovers.
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
@Input() color = 'yellow'
constructor(private el: ElementRef) { }
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.color);
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight('');
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
BorderDirective
- a similar directive that will apply a border of a specified color to the element whenever it hovers
import { Directive, ElementRef, HostListener, Input, OnInit } from '@angular/core';
@Directive({
selector: '[appBorder]',
standalone: true,
})
export class BorderDirective implements OnInit {
@Input() color: string = 'red'
constructor(private el: ElementRef) {
}
ngOnInit() {
this.border('')
}
@HostListener('mouseenter') onMouseEnter() {
this.border(this.color);
}
@HostListener('mouseleave') onMouseLeave() {
this.border('');
}
private border(color: string) {
this.el.nativeElement.style.border = `2px solid ${color || 'transparent'}`;
}
}
We can now easily apply our directives to any element we want ie. a paragraph:
<p appHighlight>Paragraph with a highlight directive</p>
<p appBorder>Paragraph with a border directive</p>
However, if we wanted to apply both highlighting and border on hover we would need to add both directives explicitly:
<p appBorder appHighlight> Paragraph with a highlight and border directive</p>
With the new directives composition API, we can easily create another directive that composes behaviors of our 2 directives.
Host Directives
Angular 15 added a new property to the @Directive
and @Component
decorators. In this property, we can specify an array of different directives that we want our new component or directive to apply on a host element. We can do it as follows:
@Directive({
selector: '[appHighlightAndBorder]',
hostDirectives: [HighlightDirective, BorderDirective],
standalone: true,
})
export class HighlightAndBorderDirective {}
As you can see in the above example, by just defining the hostDirectives
property containing our highlight and border directives, we created a new directive that composes both behaviors into one directive. We can now achieve the same result as in Fig. 4 by using just a single directive:
<p appHighlightAndBorder>Paragraph with a highlight and border directive using a single composed directive</p>
Passing inputs and outputs
Our newly composed directive works nicely already, but there is a problem. How do we pass properties to the directives that are defined in the hostDirectives
array? They are not passed by default, but we can configure them to do so pretty easily by using an extended syntax:
hostDirectives: [
{
directive: HighlightDirective,
inputs: ['color'],
},
{
directive: BorderDirective,
inputs: ['color'],
},
],
This syntax takes exposes the "inner" directives` color
input from the HighlightAndBorderDirective
, and passes them down to both highlight and border directives.
<p appHighlightAndBorder color="blue">Paragraph with a highlight and border directive using a single composed directive</p>
This works, but we ended up with a border and highlight color both being blue. Luckily Angular's API allows us to easily redefine the properties' names using the <innerPropName>: <outerPropName>
syntax. So let's remap out properties to highlightColor
and borderColor
so that the names don't collide with each other:
hostDirectives: [
{
directive: HighlightDirective,
inputs: ['color: highlightColor'],
},
{
directive: BorderDirective,
inputs: ['color: borderColor'],
},
],
Now we can control both colors individually:
<p appHighlightAndBorder borderColor="blue" highlightColor="lightskyblue">Paragraph with a highlight and border directive using a single composed directive</p>
We could apply the same approach to mapping directive's outputs eg.
outputs: ['stateChanged'],
or
outputs: ['stateChanged: changed'],
Adding host directives to a component
Similarly to composing a directive out of other directives, we can apply the same approach to adding behavior to components using hostDirectives
API. This way, we could, for example, create a more specialized component or just apply the behavior of the directive to a whole host element:
@Component({
selector: 'app-highlight-and-border',
standalone: true,
template: `
<p>My first component with border and highlight</p>
`,
styles: [
`
:host {
cursor: pointer;
display: block;
padding: 16px;
width: 500px;
}
`,
],
hostDirectives: [HighlightDirective, BorderDirective],
})
export class HighlightAndBorderComponent {}
This component will render the paragraph, and apply both directives' behavior to the host element:
<app-highlight-and-border></app-highlight-and-border>
Just like we did for the directive, we can also expose and remap the directives inputs using the extended syntax. But if we would like to access and modify the directives inputs from within our component, we can also do that. This is where Angular's dependency injection comes in handy. We can inject the host directives via a constructor just like we would do for a service. After we have the directives instances available, we can modify them ie. in the ngOnInit
lifecycle hook:
export class HighlightAndBorderComponent {
constructor(
public highlight: HighlightDirective,
public border: BorderDirective
) {}
ngOnInit(): void {
this.highlight.color = 'lightcoral';
this.border.color = 'red';
}
}
With this change, the code from Fig. 13 will use lightcoral
as a background color and red
as a border color.
Performance Note
While this API gives us a powerful tool-set for reusing behaviors across different components, it can impact the performance of our application if used excessively. For each instance of a given composed component Angular will create objects of the component class itself as well as an instance of each directive that it is composed of.
If the component appears only a couple of times in the application. then it won't make a significant difference. However, if we create, for example, a composed checkbox component that appears hundreds of times in the app, this may have a noticeable performance impact. Please make sure you use this pattern with caution, and profile your application in order to find the right composition pattern for your application.
Summary
As I have shown in the above examples, the directives composition API can be a quite useful but easy-to-use tool for extracting behaviors into smaller directives and combining them into more complex behaviors.
In case you have any questions, you can always tweet or DM me at @ktrz. I'm always happy to help!