🚀 Stop Extending Components! Use Angular’s Directive Composition API!

Felipe Norato
2 min readFeb 4, 2025

--

Inheritance in Angular can lead to tight coupling and hard-to-maintain code. With the Directive Composition API, we can compose behaviors into a component using hostDirectives, making it more reusable and flexible.

Let’s refactor a simple button component step by step!

❌ The Old Way: Using Directives Manually

In this approach, we manually apply the directives to the template.

Loading Directive (Disables the button when loading)

import { Directive, input, HostBinding } from '@angular/core';

@Directive({
selector: '[appLoading]',
standalone: true,
})
export class LoadingDirective {
appLoading = input<boolean>(false);

@HostBinding('disabled') get isDisabled() {
return this.appLoading();
}
}

Analytics Directive (Emits an event when clicked)

import { Directive, input, output, HostListener } from '@angular/core';

@Directive({
selector: '[appAnalytics]',
standalone: true,
})
export class AnalyticsDirective {
eventName = input<string>('');
eventTracked = output<string>();

@HostListener('click') onClick() {
console.log(`Button Clicked: ${this.eventName()}`);
this.eventTracked.emit(this.eventName());
}
}

Button Component (Without hostDirectives)

Here, we apply the directives directly in the template.

import { Component, input, output } from '@angular/core';
import { LoadingDirective } from './loading.directive';
import { AnalyticsDirective } from './analytics.directive';

@Component({
selector: 'app-button',
template: `
<button [appLoading]="loading()" [appAnalytics]="eventName()" (eventTracked)="buttonEventTracked.emit($event)">
<ng-content></ng-content>
</button>
`,
standalone: true,
imports: [LoadingDirective, AnalyticsDirective], // Manually import directives
})
export class ButtonComponent {
loading = input<boolean>(false);
eventName = input<string>('');
buttonEventTracked = output<string>();
}

âś… Works fine, but:

  • We need to import all directives manually.
  • Directive bindings clutter the template.
  • Every new directive must be manually added to imports and the template.

âś… The Better Way: Using hostDirectives

With hostDirectives, we move directive logic inside the component, making it cleaner and easier to extend.

import { Component, output } from '@angular/core';
import { LoadingDirective } from './loading.directive';
import { AnalyticsDirective } from './analytics.directive';

@Component({
selector: 'app-button',
template: `<button><ng-content></ng-content></button>`,
standalone: true,
hostDirectives: [
{ directive: LoadingDirective, inputs: ['appLoading: loading'] },
{ directive: AnalyticsDirective, inputs: ['eventName'], outputs: ['eventTracked: buttonEventTracked'] },
],
})
export class ButtonComponent {
buttonEventTracked = output<string>();
}

âś… Why is this better?

  • Cleaner template (no more [appLoading] and [appAnalytics]).
  • No need to import directives manually in imports.
  • Directives are attached automatically, making the component more modular.

Usage (Both Versions Work the Same!)

<app-button [loading]="isLoading" [eventName]="'Submit Clicked'" (buttonEventTracked)="handleEvent($event)">
Submit
</app-button>

🔥 Why Use hostDirectives?

  • No manual directive imports — directives are automatically added.
  • Cleaner, more readable templates.
  • Easier to extend — just add another directive in hostDirectives.

💡 Are you still manually applying directives? Try hostDirectives and make your Angular components simpler and more scalable! 🚀

What do you think? Let’s discuss below! 👇

--

--

Felipe Norato
Felipe Norato

Written by Felipe Norato

A person who likes to solve people’s lives using Code and sometimes play Guitar. Lover of TV Shows and Movies, as well as beautiful and performative code.

Responses (3)