Applying multiple directives to the same element using the Directive Composition API
In this recipe, you’ll use the Directive Composition API to create multiple components and apply directives to them directly for reusability instead of having to apply the directives to each component or create additional elements inside the template of the component to apply the directives.
Getting ready
The app that we are going to work with resides in start/apps/chapter02/ng-directive-comp-api
inside the cloned repository:
- Open the code repository in your code editor.
- Open the terminal, navigate to the code repository directory, and run the following command to serve the project:
npm run serve ng-directive-comp-api
This should open the app in a new browser tab, and you should see the following:
Figure 2.13: ng-directive-comp-api app running on http://localhost:4200
How to do it…
- First, we’ll create a couple of components for our application. We’ll create one directive for the filled button, one for the outline button, and one for a button with a tooltip. Run the following command from the
start
folder within the workspace:nx g directive button-filled --directory apps/chapter02/ng-directive-comp-api/src/app/directives --standalone=false nx g directive button-outlined --directory apps/chapter02/ng-directive-comp-api/src/app/directives --standalone=false nx g directive button-with-tooltip --directory apps/chapter02/ng-directive-comp-api/src/app/directives --standalone=false
If asked, choose the
@nx/angular:component schematics
and choose the “As provided” action.Note that all the directives we have created are non-standalone directives. That is because the application is bootstrapped with an
NgModule
and theAppComponent
is not a standalone component. Therefore, we these directives to be imported in theapp.module.ts
for this recipe to work.
- Let’s make the
ButtonDirective
a standalone directive, which means this isn’t going to be a part of anyNgModule
. Update thebutton.directive.ts
as follows:... @Directive({ selector: '[appButton]', standalone: true, }) export class ButtonDirective { ... }
- Let’s also remove it from the
app.module.ts
file as it is now astandalone
directive. Update theapp.module.ts
file as follows:... import { ButtonDirective } from './directives/button.directive'; // <-- remove the import ... @NgModule({ declarations: [ ..., ButtonDirective, // <-- remove this ... ], ... }) export class AppModule {}
You’ll notice that none of the buttons have the required styles anymore as follows:
Figure 2.14: Styles from the button directive are gone
- Let’s update the
ButtonFilledDirective
to use theButtonDirective
using the Directive Composition API. Update thebutton-filled.directive.ts
file as follows:import { Directive, HostBinding } from '@angular/core'; import { ButtonDirective } from './button.directive'; @Directive({ selector: '[appButtonFilled]', hostDirectives: [ { directive: ButtonDirective, inputs: ['color'], }, ], }) export class ButtonFilledDirective { @HostBinding('attr.fill') fill = 'filled'; }
- We can use the
appButtonFilled
directive in theapp.component.html
file as follows:... <main class="content" role="main"> <ul class="flex flex-col"> <li class="flex gap-4 items-center border-b justify- between border-slate-300 py-3">...</li> <li class="flex gap-4 items-center border-b justify- between border-slate-300 py-3"> <h4 class="text-lg">Filled Button:</h4> <button appButtonFilled color="yellow">Click Me</button> </li> <li class="flex gap-4 items-center border-b justify- between border-slate-300 py-3">...</li> <li class="flex gap-4 items-center border-b justify- between border-slate-300 py-3">...</li> </ul> </main>
Notice that we’ve removed the fill
attribute from the element.
- Let’s update the
ButtonOutlined
directive as well. We’ll modify thebutton-outlined.directive.ts
as follows:import { Directive, HostBinding } from '@angular/core'; import { ButtonDirective } from './button.directive'; @Directive({ selector: '[appButtonOutlined]', hostDirectives: [ { directive: ButtonDirective, inputs: ['color'], }, ], }) export class ButtonOutlinedDirective { @HostBinding('attr.fill') fill = 'outlined'; }
- Let’s also modify the
ButtonWithTooltipDirective
class. We’ll update thebutton-with-tooltip.directive.ts
as follows:import { Directive } from '@angular/core'; import { ButtonDirective } from './button.directive'; import { TooltipDirective } from './tooltip.directive'; @Directive({ selector: '[appButtonWithTooltip]', hostDirectives: [ { directive: ButtonDirective, inputs: ['color', 'fill'], }, { directive: TooltipDirective, inputs: ['appTooltip: tooltip'], }, ], }) export class ButtonWithTooltipDirective {}
You will notice that the app starts throwing an error that
TooltipDirective
is not a standalone component. That’s true. We need to do the same thing we did for theButtonDirective
in step 2 and step 3 for theTooltipDirective
as well. Move on to the next step once you’ve done that.
- Now, update the
app.component.html
file to use both theappButtonOutlined
andappButtonTooltip
directives as follows:... <main class="content" role="main"> <ul class="flex flex-col"> <li class="flex gap-4 items-center border-b justify- between border-slate-300 py-3">...</li> <li class="flex gap-4 items-center border-b justify- between border-slate-300 py-3">...</li> <li class="flex gap-4 items-center border-b justify- between border-slate-300 py-3"> <h4 class="text-lg">Outlined Button:</h4> <button appButtonOutlined>Click Me</button> </li> <li class="flex gap-4 items-center border-b justify- between border-slate-300 py-3"> <h4 class="text-lg">Button with Tooltip:</h4> <div class="flex flex-col gap-4"> <button appButtonWithTooltip tooltip="code with ahsan" fill="outlined" color="blue"> Click Me </button> <button appButtonWithTooltip tooltip="code with ahsan" fill="filled" color="blue"> Click Me </button> </div> </li> </ul> </main>
If you’ve followed all the steps correctly, you should be able to see the final result as follows:
Figure 2.15: Final result containing buttons with different directives applied
How it works…
The Directive Composition API was introduced in Angular v15 and has been one of the most requested features from the Angular community. In this recipe, we tried to create some components that bind the directives to the component directly in the component’s TypeScript classes rather than in the template. This eliminates the need to create a wrapper element within the components to then apply the directives or to map the inputs of the components to the inputs of the directives. This also allows multiple directives to be bound to the same component – even if they may have inputs with the same names, we can alias them differently.
The flow of the directives in our application works in the following way:
- The
AppComponent
uses theButtonFilledDirective
,ButtonOutlinedDirective
, andButtonWithTooltipDirective
directives. For this, these directive need to be non-standalone since the application is bootstrapped with anNgModule
ButtonFilledDirective
,ButtonOutlinedDirective
, andButtonWithTooltipDirective
directives use the directive composition API to use theButtonDirective
and theTooltipDirective
. These need to be standalone directives to be used as ‘hostDirectives
'
The key to using the Directive Composition API is to construct your base-directives with the standalone: true
flag. This means your directives aren’t part of any NgModule
and can be imported directly into the imports array of any component they’re being used in. This is why we make both the ButtonDirective
and the TooltipDirective
standalone in steps 2, 3, and 7. Then, we use those directives in ButtonFilledDirective
, ButtonOutlinedDirective
, and ButtonWithTooltipDirective
to be able to reuse the logic without having to create any wrapper component or additional HTML. We do it using the hostDirectives
property in the directive metadata. Notice that we pass an array of objects to this property and each object can contain the directive
property, which takes the class of the directive
to be applied. And we can also provide inputs and outputs for the host bindings. As you saw for the ButtonWithTooltipDirective
, we also aliased the appTooltip
input of the TooltipDirective
with the tooltip
input of the ButtonWithTooltipDirective
. One thing to notice is that if you don’t want to map any inputs or outputs and just want to bind a directive in the hostDirectives
, you can just provide an array of the classes of the directives to be applied as follows:
hostDirectives: [
ButtonDirective,
TooltipDirective
],
See also
- Directive Composition API documentation: https://angular.io/guide/directive-composition-api#directive-composition-api
- Standalone components: https://angular.io/guide/standalone-components
Learn more on Discord
To join the Discord community for this book – where you can share feedback, ask questions to the author, and learn about new releases – follow the QR code below:
https://packt.link/AngularCookbook2e