DEV Community

Cover image for Directive Composition with Angular 15
Nicolas Frizzarin for This is Angular

Posted on

 

Directive Composition with Angular 15

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.

Top comments (9)

Collapse
 
bleuscyther profile image
jeffrey n. carre

When composing inputs I have an error on the HTML.
Is it the IDE or Angular complaining about: Unknown property:

Image description

Collapse
 
nicoss54 profile image
Nicolas Frizzarin

By default all inputs or outputs of your host directives are not exposed.
Is this input exposed ? Which IDE do you use ? Vscode ?

Collapse
 
bleuscyther profile image
jeffrey n. carre

I am using webstorm 2022.3. You might be right about the exposing part.
If i re-declare the IO at the level of the Component it stops complaining

Thread Thread
 
nicoss54 profile image
Nicolas Frizzarin • Edited

Normally, if you expose your inputs and outputs in the property inputs or outputs of the hostDirective as in the example in the article, there is no error in VsCode for example.

The angular plugin of Webstom is maybe not up to date with the last version of Angular and the directive composition feature. I know that Jetbrains recreate the Angular plugin from scratch to have better refactoring than the Angular plugin for Vscode. This have as impacts that sometimes, the plugin is not up to date when the last version of Angular is out

Thread Thread
 
bleuscyther profile image
jeffrey n. carre

In order to remove the error I have to kind of duplicate the declaration, It's also the only way I get to use the value form the directive. I am trying to see if there is a way to access the HostDirective , I think it would be simpler, but @ViewChild does not seem to work well.

Image description

I have found this uniq video whit more details

youtube.com/watch?v=oC9Qd9yw3pE

Thread Thread
 
bleuscyther profile image
jeffrey n. carre

Yup definitely a gem, πŸ˜‚. This Video should be the official docs.

Thread Thread
 
nicoss54 profile image
Nicolas Frizzarin

Hey, in the following you can see an example that works as expected
This is the link: stackblitz.com/edit/angular-ivy-hx...

As you will see, i don't duplicate the my inputs and @ViewChild work as expected :)

Enjoy the link :)

Collapse
 
michaill profile image
Michal Illovsky

Did you try to bind structural directive as hostDirective? I still get an error TemplateRef not found -> stackoverflow.com/questions/752600...

Collapse
 
free_one profile image
Michael

straight forward

typescript

11 Tips That Make You a Better Typescript Programmer

1 Think in {Set}

Type is an everyday concept to programmers, but it’s surprisingly difficult to define it succinctly. I find it helpful to use Set as a conceptual model instead.

#2 Understand declared type and narrowed type

One extremely powerful typescript feature is automatic type narrowing based on control flow. This means a variable has two types associated with it at any specific point of code location: a declaration type and a narrowed type.

#3 Use discriminated union instead of optional fields

...

Read the whole post now!