Angular 15: Directive Composition in Forms

Angular operates on a six-month release cadence, and version 15 is nearly here. Looking back, Angular 14 introduced several significant capabilities:

  • standalone components
  • composition pattern
  • typed forms, among others

The introduction of standalone components reshaped the Angular ecosystem considerably, and that influence persists into version 15. This release brings a new API into play: the directive composition API.

The challenge

Directives rank among Angular's most essential and potent features. The framework recognizes three distinct directive categories:

  • structural directives, which shape the DOM structure
  • attribute directives, which enhance individual DOM elements
  • components, which are directives paired with templates

Angular Material's MatTooltip directive serves as a typical illustration. This directive presents supplementary information when a user hovers over an element, such as text.

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

Suppose you want to construct a button that reveals extra details upon hover. Drawing from common practices in Angular libraries, the recommended approach involves crafting an attribute directive that augments the standard button.

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

This scenario involves considerable boilerplate. Developers are expected to recall using the MatTooltip directive and configuring it appropriately. In a perfect world, the developer experience would allow code like this:

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

Achieving this kind of code presents two possible routes:

  • subclass the MatTooltip class, though single inheritance restricts compositional flexibility
  • implement a mixins approach similar to Vue Js, which can become unwieldy due to side effects. Angular Material itself adopts this pattern, as shown here

The resolution

To address this issue and further enhance developer experience, the Angular team is introducing a new feature called the Directive Composition API. This API facilitates straightforward composition of directives with one another.

Consequently, both the @Directive and @Component decorators now accept a new property named hostDirectives. This property can receive 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 host directives' inputs or outputs are exposed on the host element. To make them accessible, you must explicitly list them within the inputs or outputs properties, respectively.

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

This new API also supports renaming 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

Armed with this API, you can now express the desired functionality effortlessly:

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

Unsurprisingly, this syntax applies equally to outputs. One further crucial detail: host directives are also discoverable by view and content queries, enabling patterns like the following:

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

Summary

Version 15's new API dramatically simplifies combining directives. You only need to leverage the new hostDirective property. Just be aware that to compose directives together, each directive must be marked as standalone.