Angular Directive composition
Let's take a look at one of the most awaited Angular features
Kevin Kreuzer
@kreuzercode
Nov 1, 2022
6 min read
Angular 15 is on the horizon, and with it, many great features. One I am incredibly excited about is directive composition.
And I am not the only one. The directive composition has been one of the most upvoted Angular issues on GitHub. Let's see what it's all about.
To explain directive composition, we will look at an actual use case in the form of a digital pinboard. We want to implement a pinboard that displays pins. Each pin should display an info text as a tooltip on hover. Furthermore, each pin can be dragged and dropped and initially rotated.
Digital pinboard with pinsThe code for such an application might look something like this.
<pinboard>
<pin image="rocket"></pin>
<pin image="beer"></pin>
<pin image="keyboard"></pin>
<pin image="testing"></pin>
<pin image="coffee"></pin>
</pinboard>
We have a PinboardComponent
and project a bunch of pins to it. At this point, our pins will be displayed as illustrated on the graphic on top. This means they aren't yet initially rotated, nor are they draggable, nor will we display a tooltip. All features mentioned on top are missing.
Of course, we could go ahead and implement those features right in the pin component. But luckily, our code base already contains some handy directives with the desired functionality.
There's a DragableDirective
, a RotateDirective
and a TooltipDirective
at our disposal. Let's use those attribute directives to add the missing features to our pins.
<pinboard #dragZone>
<pin
rotate="45deg"
tooltip="Ship new products"
dragable
[dragzone]="pinboard"
image="rocket"
>
</pin>
<pin
rotate="-20deg"
tooltip="A good beer after a day of coding"
dragable
[dragzone]="pinboard"
image="beer"
>
</pin>
<pin
rotate="0deg"
tooltip="My favourite Keyboard, the Moonlander"
dragable
[dragzone]="pinboard"
image="keyboard"
>
</pin>
<pin
rotate="10deg"
tooltip="Write tests for better code"
dragable
[dragzone]="pinboard"
image="testing"
>
</pin>
<pin
rotate="25deg"
tooltip="No coffee no code"
dragable
[dragzone]="pinboard"
image="coffee"
>
</pin>
</pinboard>
Each pin now applies the rotate
attribute directive and passes the specified initial rotation degrees. Then there's the tooltip
attribute directive with the tooltip text, and last but not least, the dragable
attribute with an addtional dragZone
input.
The drag zone is necessary because you only want to be able to drag the pins inside the board.
Nice. This is a good approach, but it has some downsides.
To make the PinComponent
feature complete; the developer has to remember which directives are needed and has to apply all directives by himself.
Wouldn't it be cool if we could provide the PinComponent
with drag, tooltip, and rotate features right out of the box and still reuse our directives?
Why do we need directive composition?
So far, we could reuse our directives in our components by using inheritance. To get the draggable functionality, for example, we could extend our PinComponent
.
export class PinComponent extends DragableDirective implements OnInit {}
The nice thing is that by using inheritance, we can inherit all Angular features like HostBinding
or HostListeners
etc…. And it also works very well with template type checking and minifiers.
But this approach has its limitations. What about the tooltip
and the rotate
functionality? We can only extend one class, right?
Furthermore, we have no way of narrowing down the public API of the PinComponent
. The public API of directives leaks into derived classes.
This is not an optimal solution, that’s why we now get directive composition.
Follow me on Twitter because you will get notified about new Angular blog posts and cool frontend stuff!😉
Directive composition
The new directive composition API introduces a hostDirectives
property on the Components and Directives decorator.
The property value is an array of configuration objects. Each config objects contains a required directive attribute and two optional properties input
and output
.
hostDirectives?: (Type<unknown> | {
directive: Type<unknown>;
inputs?: string[];
outputs?: string[];
})[];
Let's go ahead and try to use this brand-new property in our PinComponent
to add the tooltip, rotate, and drag features.
@Component({
selector: 'pin',
template: `<img [src]="'assets/' + image + '.svg'" />`,
hostDirectives: [
{ directive: TooltipDirective },
{ directive: DragableDirective },
{ directive: RotateDirective },
],
})
export class PinComponent implements OnInit {}
With this, we can also remove the draggable
attribute directive from the pins in our HTML.
<pin
rotate="25deg"
tooltip="No coffee no code"
[dragzone]="pinboard"
image="coffee"
>
</pin>
If we would not need to pass a tooltip and rotate input, we could also remove those attributes since they are now provided by hostDirectives
on the PinComponent
. But we still need those attributes as well as the dragzone
attribute because those attributes are inputs.
Let's go ahead and run our app. Instead of excellent features, we get a bunch of compilation errors:
ERROR
src/app/pin.component.ts:20:17 - error NG2014:
Host directive TooltipDirective must be standalone 20
{directive: TooltipDirective}
Well, that's a friendly error message which informs us about one of limitations of host directives.
Host directives can only be used with standalone directives. No problem. Let's go ahead and convert our directives to standalone directives.
Standalone, what is this? Standalone components were introduced as a Developer preview in Angular 14. If you want to learn more about it check out my article on standalone components.
Angular standalone components
To convert our directives to standalone directives, we have to add the standalone
property with a value of true
in the directives decorator and move them from the declarations array to the imports array in the AppModule
.
Great, let's try it out!
PinComponent's hover state with broken tooltip. The tooltip directive gets executed, but the tooltip text is undefined.The tooltip is broken on hover, the icon is not rotated, and the pin is not draggable. Why is that? It seems like the directives input doesn't work anymore. But why? we still pass them in the HTML as attributes on the pin component!
Whenever you use hostDirectives all Inputs and Outputs are hidden by default. We explicitly have to provide the public API on the inputs
and outputs
config objects.
hostDirectives: [
{ directive: TooltipDirective, inputs: ['tooltip'] },
{ directive: DragableDirective, inputs: ['dragzone'] },
{ directive: RotateDirective, inputs: ['rotate'] },
];
This is an excellent feature since it gives us complete control over the public API of our component. Let's run our code.
Rotated and Hovered PinComponent displays a Tooltip text and can be rearranged via drag & drop.Nice, we get the tooltip on hover, we get the rotation, and of course, the Pins are draggable. All features seem to work. What about the outputs
property?
In the same way we configured our Inputs we can also configure our Outputs. Our DragableDirective
, for example, emits an event that notifies you once you grab a Pin. We can use the outputs property to include the pinGrabbed
event in our public API.
hostDirectives: [
{ directive: TooltipDirective, inputs: ['tooltip'] },
{
directive: DragableDirective,
inputs: ['dragzone'],
outputs: ['pinGrabbed'],
},
{ directive: RotateDirective, inputs: ['rotate'] },
];
Pretty exciting, right? But that's not all; there's even more.
Aliases
Another neat feature of directive composition is aliasing inputs and outputs. dragzone
is a pretty generic name for our Input. In the context of pins, it would be more accurate to name the Input pinBoard
instead of dragzone
.
Let's use the alias syntax to rename the dragzone
property on the DragableDirective
.
hostDirectives: [
// ...
{
directive: DragableDirective,
inputs: ['dragzone: pinBoard'],
outputs: ['pinGrabbed'],
},
// ...
];
Great. Once aliased, we can use the pinBoard
input on the pin.
<pin
rotate="0deg"
tooltip="My favourite Keyboard, the Moonlander"
[pinBoard]="pinboard"
image="keyboard"
(pinGrabbed)="pinGrabbed()"
>
</pin>
The aliasing works precisely the same for outputs
.
Summary
Directive composition is a unique and exciting feature that offers the following benefits.
We can apply as many directives to the host as we want. There are no limitations.
By default, all the
Inputs
andOutputs
are hidden. We can use theinputs
andoutputs
properties to include in our public API and make them visible.Directive composition works with template type checking.
All directive features, such as
HostBinding
, Injection Tokens, etc... work with directive composition.Host directives can be chained. You can have host directives that are built on other host directives.
As great as it is, it also has some limitations:
As discovered throughout the post, host directives have to be standalone.
Only one directive can match a component. Make sure to use a directive only once inside a chain.
Components can not be used as host directives.
Do you enjoy the theme of the code preview? Explore our brand new theme plugin
Skol - the ultimate IDE theme
Northern lights feeling straight to your IDE. A simple but powerful dark theme that looks great and relaxes your eyes.
Do you enjoy the content and would like to learn more about how to ensure long term maintainability of your Angular application?
Angular Enterprise Architecture eBook
Learn how to architect a new or existing enterprise grade Angular application with a bulletproof tooling based automated architecture validation.
This will ensure that Your project stays maintainable, extendable and therefore with high delivery velocity over the whole project lifetime!
Get notified
about new blog posts
Sign up for Angular Experts Content Updates & News and you'll get notified whenever we release a new blog posts about Angular, Ngrx, RxJs or other interesting Frontend topics!
We will never share your email with anyone else and you can unsubscribe at any time!
Responses & comments
Do not hesitate to ask questions and share your own experience and perspective with the topic
Nivek
Google Developer Expert (GDE)
for Angular & Web Technologies
A trainer, consultant, and senior front-end engineer with a focus on the modern web, as well as a Google Developer Expert for Angular & Web technologies. He is deeply experienced in implementing, maintaining and improving applications and core libraries on behalf of big enterprises worldwide.
Kevin is forever expanding and sharing his knowledge. He maintains and contributes to multiple open-source projects and teaches modern web technologies on stage, in workshops, podcasts, videos and articles. He is a writer for various tech publications and was 2019’s most active Angular In-Depth publication writer.
58
Blog posts
2M+
Blog views
39
NPM packages
4M+
Downloaded packages
100+
Videos
15
Celebrated Champions League titles
You might also like
Check out following blog posts from Angular Experts to learn even more about related topics like Angular !
Top 10 Angular Architecture Mistakes You Really Want To Avoid
In 2024, Angular keeps changing for better with ever increasing pace, but the big picture remains the same which makes architecture know-how timeless and well worth your time!
Tomas Trajan
@tomastrajan
Sep 10, 2024
15 min read
Angular Signal Inputs
Revolutionize Your Angular Components with the brand new Reactive Signal Inputs.
Kevin Kreuzer
@kreuzercode
Jan 24, 2024
6 min read
Improving DX with new Angular @Input Value Transform
Embrace the Future: Moving Beyond Getters and Setters! Learn how to leverage the power of custom transformers or the build in booleanAttribute and numberAttribute transformers.
Kevin Kreuzer
@kreuzercode
Nov 18, 2023
3 min read
Empower your team with our extensive experience
Angular Experts have spent many years consulting with enterprises and startups alike, leading workshops and tutorials, and maintaining rich open source resources. We take great pride in our experience in modern front-end and would be thrilled to help your business boom