An Introduction to the Angular Directive Composition API
Posted: 12/7/2022
Categories:
angular
Angular 15 introduced new functionality with the Directive Composition API that has got the Angular community in a bit of a tiz of excitement. I was curious to find out what all the fuss was about.
The new API introduces a new way to apply directives to components.
Benefits
With the new approach it'll enable you to:
- reduce code duplication
- combine directives to form composite directives
Putting the new API to use
Basic Example
Before we see the new code in action, lets first look at a basic example where we take a directive applied in a traditional way, and instead apply it using the Directive Composition API.
Traditionally you would add a directive as follows:
<my-component my-directive></my-component>
With the new API, rather than apply it via the HTML, you instead apply it within the component's decorator:
@Component({
selector: "my-component",
template: "my-component.html",
hostDirectives: [MyDirective],
})
export class MyComponent {}
Practical Example
Now let's look at a slightly more practical example, and introduce some new concepts along the way.
We want to add a directive called TooltipDirective
.
This directive will display a tooltip on the component it's applied to.
First of all, we specify the directive using the hostDirectives
property:
@Component({
selector: "my-component",
template: "my-component.html",
hostDirectives: [TooltipDirective],
})
export class MyComponent {}
This doesn't achieve much though. Our tooltip directive needs to know what text to display for a tooltip.
Accepting Inputs
To do this our directive exposes an input named text
.
Using the traditional approach, we would apply it as follows:
<my-component appTooltip [text]="'My tooltip text'"></my-component>
In order to achieve the same with the Directive Composition API, we instead specify an inputs
array:
@Component({
selector: 'my-component',
template: 'my-component.html',
hostDirectives: [
directive: TooltipDirective,
inputs: ['text'],
}
]
})
export class MyComponent { }
What the inputs
property does is essentially say "if an attribute named [text]
is applied to the host element, associate that with the text
input on the TooltipDirective
.
So as with our traditional example we can pass in the tooltip text using the [text]
attribute:
<my-component [text]="'My tooltip text'"></my-component>
Input Aliases
In the context of the TooltipDirective
an input named Text
makes sense. It doesn't need to be named TooltipText
as it is on the TooltipDirective
.
But when you see the [Text]
attribute applied to the host component, [Text]
doesn't tell us much. We don't really know how that attribute is used.
To make our code more readable, we can use an alias to represent our Text
input:
@Component({
selector: 'my-component',
template: 'my-component.html',
hostDirectives: [
directive: TooltipDirective,
inputs: ['text:tooltipText'],
}
]
})
export class MyComponent { }
With the above code we are saying "expose the input text
using the attribute tooltipText
". Now when we pass in the attribute, the code provides a clear indication of how it is used:
<my-component [tooltipText]="'My tooltip text'"></my-component>
Exposing Outputs
As well as simply displaying a tooltip, TooltipDirective
also provides a "Want to know more...?" link. It provides the user of the directive with a way to provide additional information when the user clicks the 'Want to know more...?" text.
But because we don't know what context the directive is going to be used in, the directive itself doesn't simply open a link. What if for example we want a modal displayed instead? Therefore the directive provides an Output we can subscribe to. That output is triggered/emitted whenever the user clicks the "Want to know more...?" link.
To use the output, we take a similar approach to how we handled inputs, this time using the outputs
array.
In the following example we expose an output named moreInfoClick
.
@Component({
selector: 'my-component',
template: 'my-component.html',
hostDirectives: [
directive: TooltipDirective,
inputs: ['text:tooltipText'],
outputs: ['moreInfoClick']
}
]
})
export class MyComponent { }
We can then subscribe to the moreInfoClick
output as we would any other output:
<my-component
[tooltipText]="'My tooltip text'"
(moreInfoClick)="showMoreInfo()"
></my-component>
We can then write the showMoreInfo()
method to open a link, display a modal or potentially something else.
Just like inputs, outputs support aliases:
@Component({
selector: 'my-component',
template: 'my-component.html',
hostDirectives: [
directive: TooltipDirective,
inputs: ['text:tooltipText'],
outputs: ['moreInfoClick:moreInfoOnTooltipClick']
}
]
})
export class MyComponent { }
Limitations
The new API does come with some limitations.
- The directives must be stand-alone.
- It's not possible to add directives at runtime.
Research Links
TODO - Maybe include in a front-matter property (array of objects) and then expose later via a custom plugin...