> ## Content Index
> Fetch the complete content index at: https://www.angularspace.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Directive composition API
- URL: https://www.angularspace.com/directive-composition-api/
- Published: 2024-09-10T06:50:41.000Z
- Updated: 2024-09-10T07:14:24.000Z
- Author: Konrad Stępień
- Tags: Articles

Writing clear and reusable code is an integral part of programming. One of the many tools that Angular offers for developing complex applications is the Directive Composition API. This feature facilitates writing of reusable and maintainable code. In this article, we will explore what the **Directive Composition API** is, how the `hostDirective` option works, and how to successfully use this capability in your Angular projects.

## Introduction

Angular Directives are classes that add additional behaviour to HTML elements in your applications. Thanks to the `hostDirective` option, developers can manage directives more efficiently and maintainable.  
  
Official documentation about **Directive Composition API** you can find under this [*link*](https://angular.dev/guide/directives/directive-composition-api?ref=angularspace.com).  
  
In this article, we will cover:

- What is the Directive Composition API?
- Understanding the `hostDirective` option
- Practical example
- Host directive semantics
- Best Practices and Tips

## What is Directive composition API?

Directive composition API lets you apply directives to a component's host element from within the component's TypeScript class. Thanks to that feature, we can create logic in separated directives and combine them into components.

## Understanding the `hostDirective` option

The option is available in angular components and directives. This provides the ability to declare and reuse business logic that can be used in multiple places.

You apply directives to a component by adding a `hostDirectives` property to a component's decorator.

```typescript
@Directive({
    // ...
    hostDirectives: [UserDirective]
})
```

Directives used in `hostDirectives` must be **Standalone**. I.e., they should have `standalone: true` set in their decorator metadata.

For the following code, let's assume that one of `UserDirective`'s tasks is to look for a button in the template to emit `userRemoved` on a `click` event in the user card.

```typescript
@Directive({standalone: true})
export class UserDirective implements AfterViewInit {
    userId = input.required<number>();
    userRemoved = output<number>();

    private elementRef = inject(ElementRef);
    private destroyRef = inject(DestroyRef);

    ngAfterViewInit() {
        this.userRemoveListener();
    }

    private userRemoveListener() {
        const removeButton =
            this.elementRef.nativeElement.querySelector('button.user-remove');

        if (!removeButton) return;

        const listener$ = fromEvent(removeButton, 'click');
        listener$
            .pipe(
                takeUntilDestroyed(this.destroyRef) // unsubscribe on destroy
            )
            .subscribe(() => this.userRemoved.emit(this.userId()));
    }

    // ...
}
```

You can explicitly include `inputs` and `outputs` in your component's API by expanding the entry in

```diff
@Component({
    selector: 'user-cart',
    template: `
      <button class="user-remove">X</button> 
      ...
    `,
    standalone: true,
-   hostDirectives: [UserDirective]
+   hostDirectives: [
+       {
+           directive: UserDirective,
+           inputs: ['userId'],
+           outputs: ['userRemoved'],
+       },
+   ],
})
```

```html
<user-cart [userId]="user.id" (userRemoved)="removeUser($event)"/>
```

In this way, we can pass data and interactions to our directive through our host.

Also, there is a way to use aliases. After declaring our `input` or `output`, we can mark our alias after the colon

```diff
@Component({
    selector: 'user-cart',
    template: `
      <button class="user-remove">X</button> 
      ...
    `,
    standalone: true,
    hostDirectives: [{
        directive: UserDirective,
+       inputs: ['userId: id'],
+       outputs: ['userRemoved: removed'],
    }]
})
export class UserCartComponent {}
```

```html
<user-cart [id]="user.id" (removed)="removeUser($event)"/>
```

## Practical example

[](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/KonradStepien/directive-composition-api/article.md?ref=angularspace.com#practical-example)Let's look at the case below.

You may also find a demo app of the following code in this [link](https://stackblitz.com/edit/dc-api?file=src%2Fmain.ts&ref=angularspace.com).

We have buttons and tag components.

![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/08/example.png)

#### Button - Component

This is how our component logic looks like.

```typescript
@Component({
    selector: 'app-btn',
    template: `<button><ng-content /></button>`,
    styleUrls: ['./button.component.scss'],
    standalone: true,
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ButtonComponent {
    type = input<'basic' | 'soft'>('basic');
    variant = input<'primary' | 'secondary'>('primary');

    @HostBinding('class')
    get hostClass(): string {
        return [this.type(), this.variant()].join(' ');
    }
}
```

#### Tag - Component

```typescript
@Component({
    selector: 'app-tag',
    template: `<small><ng-content /></small>`,
    styleUrls: ['./tag.component.scss'],
    standalone: true,
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TagComponent {
    type = input<'basic' | 'soft'>('basic');
    variant = input<'primary' | 'secondary'>('primary');

    @HostBinding('class')
    get hostClass(): string {
        return [this.type(), this.variant()].join(' ');
    }
}
```

Also, a directive used on buttons.

#### Disabled - Directive

```typescript
@Directive({
    selector: '[appBtnDisabled]',
    standalone: true,
})
export class BtnDisabledDirective {
    disabled = input(false, {
        transform: booleanAttribute,
        alias: 'appBtnDisabled', // data pass by selector 
    });

    @HostBinding('attr.disabled')
    get isabledAttr(): '' | null {
        return this.disabled() ? '' : null;
    }

    @HostBinding('class.disabled')
    get disabledClass(): boolean {
        return this.disabled();
    }

    @HostListener('click', ['$event'])
    @HostListener('dbclick', ['$event'])
    onClick(event: Event): void {
        if (this.disabled() === false) return;
        event.preventDefault();
        event.stopImmediatePropagation();
    }
}
```

And we can use these components as follows:

```html
<strong>Button / Basic</strong>
<app-btn>primary</app-btn>
<app-btn variant="secondary">secondary</app-btn>
<app-btn appBtnDisabled>primary</app-btn>
<app-btn variant="secondary" appBtnDisabled>secondary</app-btn>

<strong>Button / Soft</strong>
<app-btn type="soft">primary</app-btn>
<app-btn type="soft" variant="secondary">secondary</app-btn>
<app-btn type="soft" appBtnDisabled>primary</app-btn>
<app-btn type="soft" variant="secondary" appBtnDisabled>secondary</app-btn>

<strong>Tag / Basic</strong>
<app-tag>primary</app-tag>
<app-tag variant="secondary">secondary</app-tag>

<strong>Tag / Soft</strong>
<app-tag type="soft">primary</app-tag>
<app-tag type="soft" variant="secondary">secondary</app-tag>
```

Everything works fine, but there are some issues with that code:

- We have duplicated code between button and tag components.
- Nothing prevents us from imposing a `BtnDisabledDirective` on our component tag. And we would like to avoid such a scenario.
- For better accessibility, we should apply `disabled` value for `button` tag

Let's improve this code a bit!

## How to use hostDirective

To begin with, let's start by creating directives to handle the *type* & *variant* appearance of components

#### TypeAppearanceDirective

```typescript
// Selector is optional, in that case we don't need them
@Directive({standalone: true})
export class TypeAppearanceDirective {
    type = input<'basic' | 'soft'>('basic');

    @HostBinding('class')
    get hostClass(): string {
        return this.type();
    }
}
```

[](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/KonradStepien/directive-composition-api/article.md?ref=angularspace.com#typeappearancedirective)

[](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/KonradStepien/directive-composition-api/article.md?ref=angularspace.com#typeappearancedirective)[](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/KonradStepien/directive-composition-api/article.md?ref=angularspace.com#how-to-use-hostdirective)

[](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/KonradStepien/directive-composition-api/article.md?ref=angularspace.com#disabled---directive)

#### VariantAppearanceDirective

```typescript
// Selector is optional, in that case we don't need them
@Directive({standalone: true})
export class VariantAppearanceDirective {
    variant = input<'primary' | 'secondary'>('primary');

    @HostBinding('class')
    get hostClass(): string {
        return this.variant();
    }
}
```

**TypeAppearanceDirective** and **VariantAppearanceDirective** could be combined into one directive. So let's create this directive.

#### AppearanceDirective

```typescript
// Selector is optional, in that case we don't need them
@Directive({
    standalone: true,
    hostDirectives: [
        {
            directive: TypeAppearanceDirective,
            inputs: ['type'], // declare inputs
        },
        {
            directive: VariantAppearanceDirective,
            inputs: ['variant'], // declare inputs
        },
    ],
})
export class AppearanceDirective {}
```

Declared `inputs` in **AppearanceDirective** will be also available in a component, so we don't need to pass them again in our component, we just need to load our directive.

**AppearanceDirective** should be declared into **ButtonComponent** and **TagComponent**. But **BtnDisabledDirective** has to be declared only in **ButtonComponent**.

Also, we want to use `disabled` alias for `appBtnDisabled`.

There also is a way to `inject` values into our host. In **ButtonComponent** we should also provide `disabled` value into native button, by `disabled` signal property included in **BtnDisabledDirective**, let's mark our injection

Let's also mark our `inject` as `self` to inform the Angular that should only look for a value that is bound on the component injector.

After small changes, we should have refactored code:

#### ButtonComponent

```diff
@Component({
    selector: 'app-btn',
+   template: `<button [disabled]="disabled()" ><ng-content /></button>`,
    styleUrls: ['./button.component.scss'],
    standalone: true,
    changeDetection: ChangeDetectionStrategy.OnPush,
+   hostDirectives: [
+       AppearanceDirective, // 'inputs' are declared inside directive
+       {
+           directive: BtnDisabledDirective,
+           inputs: ['appBtnDisabled: disabled'],
+       },
+   ],
})
export class ButtonComponent {
+   disabled = inject(BtnDisabledDirective, { self: true }).disabled;
-   type = input<'basic' | 'soft'>('basic');
-   variant = input<'primary' | 'secondary'>('primary');
-
-   @HostBinding('class')
-   get hostClass(): string {
-       return [this.type(), this.variant()].join(' ');
-   }
}
```

#### TagComponent

```diff
@Component({
    selector: 'app-tag',
    template: `<small><ng-content /></small>`,
    styleUrls: ['./tag.component.scss'],
    standalone: true,
    changeDetection: ChangeDetectionStrategy.OnPush,
+   hostDirectives: [
+     AppearanceDirective, // 'inputs' are declared inside directive
+   ],
})
export class TagComponent {
-   type = input<'basic' | 'soft'>('basic');
-   variant = input<'primary' | 'secondary'>('primary');
-
-   @HostBinding('class')
-   get hostClass(): string {
-       return [this.type(), this.variant()].join(' ');
-   }
}

```

and now we are using these components like this

```diff
<strong>Button / Basic</strong>
<app-btn>primary</app-btn>
<app-btn variant="secondary">secondary</app-btn>
-<app-btn appBtnDisabled>primary</app-btn>
+<app-btn disabled>primary</app-btn>
-<app-btn variant="secondary" appBtnDisabled>secondary</app-btn>
+<app-btn variant="secondary" disabled>secondary</app-btn>

<strong>Button / Soft</strong>
<app-btn type="soft">primary</app-btn>
<app-btn type="soft" variant="secondary">secondary</app-btn>
-<app-btn type="soft" appBtnDisabled>primary</app-btn>
+<app-btn type="soft" disabled>primary</app-btn>
-<app-btn type="soft" variant="secondary" appBtnDisabled>secondary</app-btn>
+<app-btn type="soft" variant="secondary" disabled>secondary</app-btn>

<strong>Tag / Basic</strong>
<app-tag>primary</app-tag>
<app-tag variant="secondary">secondary</app-tag>

<strong>Tag / Soft</strong>
<app-tag type="soft">primary</app-tag>
<app-tag type="soft" variant="secondary">secondary</app-tag>
```

The `disabled` input is now an integral part of `ButtonComponent` and we don't need to import `BtnDisabledDirective` in **TagComponent**.

## Host directive semantics

### Directives execution order

Host directives just as the other components and directives have the same lifecycle used directly in a template. Remember, host directives always execute their constructor, lifecycle hooks, and bindings before the host.

It is important to keep this in mind to be sure and know how to avoid causing our application's performance to suffer. Based on our example with the button component

```typescript
@Directive({
    standalone: true,
    hostDirectives: [
        {
            directive: TypeAppearanceDirective,
            inputs: ['type'],
        },
        {
            directive: VariantAppearanceDirective,
            inputs: ['variant'],
        },
    ],
})
export class AppearanceDirective {}

@Component({
    selector: 'app-btn',
    template: `<button><ng-content /></button>`,
    styleUrls: ['./button.component.scss'],
    standalone: true,
    changeDetection: ChangeDetectionStrategy.OnPush,
    hostDirectives: [
        AppearanceDirective,
        {
            directive: BtnDisabledDirective,
            inputs: ['appBtnDisabled: disabled'],
        },
    ],
})
export class ButtonComponent {
    // ...
}
```

[](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/KonradStepien/directive-composition-api/article.md?ref=angularspace.com#directives-execution-order)the order of execution will look like this:

1. `TypeAppearanceDirective` instantiated
2. `VariantAppearanceDirective` instantiated
3. `AppearanceDirective` instantiated
4. `BtnDisabledDirective` instantiated
5. `ButtonComponent` instantiated
6. `TypeAppearanceDirective` lifecycle hooks
7. `VariantAppearanceDirective` lifecycle hooks
8. `AppearanceDirective` lifecycle hooks
9. `BtnDisabledDirective` lifecycle hooks
10. `ButtonComponent` lifecycle hooks
11. `TypeAppearanceDirective` applies host bindings
12. `VariantAppearanceDirective` applies host bindings
13. `AppearanceDirective` applies host bindings
14. `BtnDisabledDirective` applies host bindings
15. `ButtonComponent` applies host bindings

As you can see, that the component and host directives create a new instance each time. In this case, their number is 5, so to maintain good performance of our application, we should not abuse this functionality and implement logic that does not require a lot of computing power.

### Dependency injection

Host can inject the instances of host directives that specify `hostDirectives`, and vice versa.

Again, based on the example with `ButtonComponent`, we just need to define our Dependency injection by class.

```typescript
@Component({
    selector: 'app-btn',
    template: `<button [disabled]="disabled()" ><ng-content /></button>`,
    styleUrls: ['./button.component.scss'],
    standalone: true,
    changeDetection: ChangeDetectionStrategy.OnPush,
    hostDirectives: [
        // ...
        {
            directive: BtnDisabledDirective,
            inputs: ['appBtnDisabled: disabled'],
        },
    ],
})
export class ButtonComponent {
    disabled = inject(BtnDisabledDirective, {self: true}).disabled
}
```

[](https://github.com/danielglejzner/angular-space-writer-mentorship-program/blob/main/KonradStepien/directive-composition-api/article.md?ref=angularspace.com#dependency-injection)The providers provided by class with `hostDirectives` take precedence over providers defined by the host directives if a host with `hostDirectives` and those host directives both supply the same injection token.

## Best Practices and Tips

- `selector` property is an optional option in directives. If we want to implement directives only in the `hostDirectives` option, we do not need to pass this parameter.
- Angular detects whether our directive contains the declared elements in `inputs` and `outputs` so we will be notified if there is a bad implementation.
- Use `self` flag while `injecting` directive into host.

## Summary

The **Directive Composition API** in Angular offers a way to write reusable and maintainable code. This makes it easy for us to follow the "**Don't repeat yourself**" (*DRY*) principle. However, keep in mind how the code works under the hood to avoid performance problems.

---

![](https://storage.ghost.io/c/3f/63/3f6333b8-1f83-4017-8426-91bfbc264d81/content/images/2024/09/Screenshot-2024-09-10-at-09.13.55.png)