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

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@nivekcode

Nov 1, 2022

6 min read

Angular Directive composition
share

Angular 15 is approaching, and it brings along a host of compelling features. Among them, directive composition stands out as something I'm particularly eager to explore.

The excitement isn't just mine, though. This capability has ranked among the most requested Angular features on GitHub. Time to dig into what it offers.

To demonstrate directive composition, let's build a digital pinboard. The board will hold pins, each showing an info text as a tooltip when hovered. Additionally, every pin needs to support drag-and-drop and come with an initial rotation.

Digital pinboard with a couple of displayed pins Digital pinboard with pins

Here's a rough sketch of what the application code might resemble.

<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 that projects several pins onto it. Right now, those pins appear exactly as in the graphic above—no rotation, no drag capability, and no tooltips. Every feature we outlined is still absent.

We could embed those behaviors directly into the pin component itself. But our codebase happens to already include some convenient directives that deliver the needed functionality.

At our disposal are a DragableDirective, a RotateDirective, and a TooltipDirective. Applying these attribute directives is all it takes to bring the pins to life.

<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>

Now, each pin uses the rotate attribute directive with the desired starting angle. The tooltip directive supplies the hover text, and the dragable attribute comes with an extra dragZone input.

The drag zone is necessary because you only want to be able to drag the pins inside the board.

This works well enough, but it's not without drawbacks.

Anyone building a PinComponent has to know exactly which directives are required and manually attach every one of them.

Wouldn't it be better if the PinComponent shipped with drag, tooltip, and rotate features built in, while still relying on our existing directives?

Why do we need directive composition?

Up until now, inheritance was the standard way to reuse directives within components. For instance, to get drag behavior, we might extend the PinComponent.

export class PinComponent extends DragableDirective implements OnInit {}

The upside of inheritance is that it carries over everything Angular-related, such as HostBinding or HostListeners. It also plays nicely with template type checking and minifiers.

Yet this technique hits walls quickly. How would we handle both tooltip and rotate? Only a single class can be extended.

Another issue is the lack of control over the PinComponent public API; inherited directives expose their entire interface.

Given these shortcomings, directive composition arrives as the answer.

Follow me on Twitter because you will get notified about new Angular blog posts and cool frontend stuff!😉

Directive composition

The directive composition API adds a hostDirectives property to the Components and Directives decorator.

You set this property to an array of config objects. Each such object includes a mandatory directive reference plus two optional fields, input and output.

hostDirectives?: (Type<unknown> | {
  directive: Type<unknown>;
  inputs?: string[];
  outputs?: string[];
})[];

Let's put this new property into action on our PinComponent, wiring in 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 {}

Once that's done, we can strip the draggable attribute directive from the pins in the template.

<pin
  rotate="25deg"
  tooltip="No coffee no code"
  [dragzone]="pinboard"
  image="coffee"
>
</pin>

If we didn't need to transmit a tooltip or rotate input, those attributes could vanish too, since host directives supply them. But we still depend on those attributes, along with dragzone, because they act as inputs.

Running the app now leads to a series of compilation errors:

ERROR

src/app/pin.component.ts:20:17 - error NG2014:
Host directive TooltipDirective must be standalone  20
{directive: TooltipDirective}

That error message is quite informative, pointing to a key constraint of host directives.

Host directives must be standalone. Fine—let's mark our directives as standalone.

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, we add the standalone property set to true in the directive decorator and shift them from the declarations array to the imports array in the AppModule.

Let's give it another go.

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 fails on hover, no rotation occurs, and dragging is impossible. Something's off with the inputs. They're still passed as attributes on the pin element in the HTML, so why aren't they working?

With hostDirectives, every Input and Output is hidden unless you explicitly opt in. The public API has to be spelled out via the inputs and outputs fields in the config.

hostDirectives: [
  { directive: TooltipDirective, inputs: ['tooltip'] },
  { directive: DragableDirective, inputs: ['dragzone'] },
  { directive: RotateDirective, inputs: ['rotate'] },
];

This design choice is beneficial because it grants full authority over the component's public surface. Let's execute the 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.

And there we go—the tooltip appears on hover, rotation is applied, and the pins can be dragged around. Everything seems operational. How about the outputs property?

Outputs can be configured just like inputs. Our DragableDirective, for instance, emits an event when a pin is grabbed. By listing the pinGrabbed event in the outputs configuration, it becomes part of the public API.

hostDirectives: [
  { directive: TooltipDirective, inputs: ['tooltip'] },
  {
    directive: DragableDirective,
    inputs: ['dragzone'],
    outputs: ['pinGrabbed'],
  },
  { directive: RotateDirective, inputs: ['rotate'] },
];

Intriguing, isn't it? And there's still more to uncover.

Aliases

Aliasing inputs and outputs is another handy aspect of directive composition. dragzone feels like a rather generic label. In a pin context, pinBoard would be a clearer name for that input.

Let's apply the alias syntax to rename dragzone on the DragableDirective.

hostDirectives: [
  // ...
  {
    directive: DragableDirective,
    inputs: ['dragzone: pinBoard'],
    outputs: ['pinGrabbed'],
  },
  // ...
];

With the alias set, the pin can now use the pinBoard input.

<pin
  rotate="0deg"
  tooltip="My favourite Keyboard, the Moonlander"
  [pinBoard]="pinboard"
  image="keyboard"
  (pinGrabbed)="pinGrabbed()"
>
</pin>

Aliasing for outputs follows the exact same pattern.

Summary

Directive composition brings a distinctive set of advantages:

  • There's no cap on how many directives you can attach to a host.

  • All Inputs and Outputs stay hidden by default, and the inputs and outputs properties let you expose them through the public API.

  • It integrates seamlessly with template type checking.

  • Everything a directive supports—HostBinding, injection tokens, and so on—functions as expected with directive composition.

  • Host directives can build on one another, forming chains of nested directives.

Of course, there are a few limitations to keep in mind:

  • As we saw, host directives must be marked as standalone.

  • Only one directive can match a component, so avoid using the same directive more than once in a chain.

  • Components cannot serve as host directives.

Do you enjoy the theme of the code preview? Explore our brand new theme plugin

Skol - the ultimate IDE theme

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.

Build smarter UIs with Angular + AI

Angular + AI Video Course

Angular + AI Video Course

A hands-on course showing how to integrate AI into Angular apps using Hash Brown to build intelligent, reactive UIs.

Learn streaming chat, tool calling, generative UI, structured outputs, and more — step by step.

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

Angular Enterprise Architecture eBook

Discover how to design a new or existing enterprise-scale Angular application using a tooling-based, automated architecture validation approach.

This methodology guarantees that your codebase remains maintainable, extendable, and thereby ensures a high delivery velocity throughout the entire project lifecycle!

Enjoying the content and want to dive deeper into Angular's new Signal Forms?

Angular Signal Forms: Hands-On Masterclass

Angular Signal Forms: Hands-On Masterclass

Get hands-on with Angular's latest Signal-Forms feature through 12 progressive chapters that combine theoretical explanations with practical labs.

Explore form fundamentals, validators, custom controls, subforms, migration paths, and much more!

Win win deal illustration

Get notified
about new blog posts

Subscribe to Angular Experts Content Updates & News, and we'll notify you every time a new blog post covering Angular, Ngrx, RxJs, or other engaging Frontend topics goes live!

We promise never to share your email with third parties, and you can unsubscribe at any moment!

Emails may include additional promotional content, for more details see our Privacy policy.

Responses & comments

Feel free to ask questions and share your own insights and experiences regarding the topic

You might also like

Browse these other blog posts from Angular Experts to deepen your understanding of related topics such as Angular !

Empower your team with our extensive experience

Angular Experts has spent years consulting with both enterprises and startups, leading workshops and tutorials, and maintaining a wealth of open source resources. We take great pride in our deep knowledge of modern front-end development and would be delighted to help your business thrive