Directive Composition with Angular 15

Nicolas Frizzarin - Nov 9 '22 - - Dev Community

Introduction

With a release cycle every 6 months, the release of Angular 15 is fast approaching.
As a reminder, the version 14 of Angular brought some big new features:

  • standalone components
  • composition pattern
  • typed forms, etc

The standalone components have had a big impact on the entire Angular ecosystem. And this feature still has impacts in version 15.
In this version, a new API will be availble: the directive composition API

The problem

Directives are one of the most important and powerful parts of Angular. There are 3 types of directives:

  • structural directives allowing to structure the DOM
  • attribute directives allowing you to enrich an element of the DOM
  • components which are simple directives without views

A concrete example of a directive could be the MatTooltip directive of Angular Material. This directive allows to display additional information to the user when he hovers over a text for example.

<div matTooltip="More information" matTooltipPosition="left">
  This is a text
</div>
Enter fullscreen mode Exit fullscreen mode

Now imagine that you want to create a button that displays additional information when the user hovers over it.

If we take inspiration from the various libraries in Angular, it's a good practice to write an attribute directive that will enrich the classic button

<button type="button" sfeirButton matTooltip="awesome button" matTooltipPosition="left">
  Click Me
</button>
Enter fullscreen mode Exit fullscreen mode

This case is too boilerplate. Developers should remember to use the MatTooltip directive and configure it correctly.

Ideally it will be awesome if developers can write someting like this:

<button type="button" sfeirButton tooltipText="awsome button" tooltipPosition="left">
  Click Me
</button>
Enter fullscreen mode Exit fullscreen mode

If we want to write this kind of code, we have two choices:

  • extend the MatTooltip class, but it's only possible to extend one class, which limits the possibilities of composition
  • to realize a mixins pattern present in Vue Js which can be complicated to maintain due to the possible side effects. For information Angular material uses this pattern as we can see here

Solution

To solve this problem, and always with the aim of improving the developer experience, the Angular team introduces a new API called: Directive Composition API.

This API will allow to compose the directives between them in a very simple way.

Now the @Directive and @Component decorator take a new property called hostDirectives.

This property takes as value an array of standalone directives or an array of configuration objects

@Directive({
  selector: "button[sfeirButton]"
  hostDirectives: [
    AwesomeDirective,
    { directive: MatTooltip,
      inputs: [],
      outputs: []
    }
  ]
})
export class SfeirButton
Enter fullscreen mode Exit fullscreen mode

By default none of the inputs or outputs of the host directives will be available on the host, unless they are specified in the inputs or outputs properties.

If you want to expose one of the inputs or outputs of the host directives, you must specify it respectively in the inputs and outputs array.

@Directive({
  selector: "button[sfeirButton]"
  hostDirectives: [
    { directive: MatTooltip,
      inputs: ['matTooltip', 'matTooltipPosition'],
      outputs: []
    }
  ]
})
export class SfeirButton
Enter fullscreen mode Exit fullscreen mode

The API also allows you to rename the exposed inputs and outputs

@Directive({
  selector: "button[sfeirButton]"
  hostDirectives: [
    { directive: MatTooltip,
      inputs: [
        'matTooltip: tooltipText',
        'matTooltipPosition: tooltipPosition
      ],
      outputs: []
    }
  ]
})
export class SfeirButton
Enter fullscreen mode Exit fullscreen mode

Thanks to this API, and with this way, it's possible now to write:

<button type="button" sfeirButton tooltipText="awsome button" tooltipPosition="left">
  Click Me
</button>
Enter fullscreen mode Exit fullscreen mode

Obviously this syntax is also valid for the outputs

One last important thing, the host directives are picked by view/content queries as well, so it's possible to write:

@ViewChild(MatToolTip) matToolTip!: MatTooltip
Enter fullscreen mode Exit fullscreen mode

Conclusion

With version 15 and the new API, it becomes very easy to compose directives together.
To compose directives between them, you just have to use the new hostDirective property.
Be careful to compose directives between them, all directives must be of type standalone.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .