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:

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.

Research Links

TODO - Maybe include in a front-matter property (array of objects) and then expose later via a custom plugin...