Angular Directive composition

Let's take a look at one of the most awaited Angular features

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@kreuzercode

Nov 1, 2022

6 min read

Angular Directive composition
share

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 a couple of displayed pins Digital pinboard with pins

The 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. 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. 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 and Outputs are hidden. We can use the inputs and outputs 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 content and think that your teammates or organization could benefit from more direct support?

Angular Enterprise Architecture Ebook

Angular Enterprise Architecture Ebook

Learn how to architect and scaffold a new enterprise grade Angular application with clean, maintainable and extendable architecture in almost no time!

Lots of actionable tips and pros & cons of specific decisions based on the extensive experience!

Get notified
about new blog posts

Sign up for Angular Experts Content Updates & News and you'll get notified whenever we release a new article 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!

Emails may include additional promotional content, for more details see our Privacy policy.
Kevin Kreuzer - GDE for Angular & Web Technologies

Kevin Kreuzer

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.

Responses

Do not hesitate to ask questions and share your own experience and perspective with the topic

You might also like

Check out following blog posts from Angular Experts to learn even more about related topics like Angular !

The Most Important Thing You Need To Understand About Angular

The Most Important Thing You Need To Understand About Angular

The Angular "Template Context" is a practically useful mental model, which once internalized, will be your main guide on the journey to build the best possible Angular applications!

emoji_objects emoji_objects emoji_objects
Tomas Trajan

Tomas Trajan

@tomastrajan

Nov 22, 2022

15 min read

Angular Material component harnesses

Angular Material component harnesses

How and why to use Angular materials component harness to write reliable, stable and readable component tests

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@kreuzercode

Nov 15, 2022

6 min read

Angular Router Standalone APIs

Angular Router Standalone APIs

Let's take a look at Angulars brand new standalone Router APIs. Is it worth it? How much can we shake off the Routers bundle?

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@kreuzercode

Oct 18, 2022

6 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