Power up host component by directive composition API in Angular
Tags: angular, webdev, frontend
Published: 2023-08-19 14:23:47

##Introduction

In this blog post, I am going to illustrate how to use directive composition API to power up host component. Directive composition API is new in Angular 15 and it allows host component to register standalone directives to modify its appearance. Moreover, Angular developers can use standalone directives as building blocks of complex directives with the help of the API. 

###What is it like without Directive Composition API

```typescript
// background-color.directive.ts
import { Directive, HostBinding, Input } from '@angular/core';

@Directive({
  selector: '[appBackgroundColor]',
  standalone: true,
})
export class BackgroundColorDirective {
  @Input()
  @HostBinding('style.background-color')
  bgColor = 'goldenrod';
}

// font-size.directive.ts
import { Directive, HostBinding, Input } from '@angular/core';

@Directive({
  selector: '[appFontSize]',
  standalone: true,
})
export class FontSizeDirective {
  @Input()
  @HostBinding('style.font-size.px')
  size = 18;
}

// hover-block.directive.ts

import { Directive, ElementRef, HostListener, inject } from '@angular/core';

@Directive({
  selector: '[appHoverBlock]',
  standalone: true,
})
export class HoverBlockDirective {

  el = inject<ElementRef<HTMLParagraphElement>>(ElementRef<HTMLParagraphElement>).nativeElement;

  @HostListener('mouseenter')
  mouseEnter() {
    this.el.style.fontWeight = 'bold';
  }

  @HostListener('mouseleave')
  mouseLeave() {
    this.el.style.fontWeight = 'normal';
  }
}
```

In my demo,  I have 3 directives:
- BackgroundColorDirective - set the background color of a block
- FontSizeDirective - set the font size of a text
- HoverBlockDirective - bold text when mouse hovers on a block

In the old way, I can apply these directives and directive inputs by specifying them on the HTML code of the host component

```typescript
// hello-username-without-api.component.ts
 
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';

@Component({
  selector: 'app-hello-username-without-api',
  standalone: true,
  template: `
    <p>Hello {{username}}!!! Hover me to bold text</p>
  `,
  styles: [`
    :host {
      display: block;
    }

    p {
      padding: 0.5rem;
    }
  `],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HelloUsernameWithoutApiComponent {
  @Input({ required: true })
  username!: string;
}
```

`HelloUsernameWithoutApiComponent` is a component that does not leverage the Directive Composition API and I want to render it in `AppComponent`.

```typescript
// main.ts

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [FormsModule, HelloUsernameWithoutApiComponent, FontSizeDirective,
  BackgroundColorDirective, HoverBlockDirective],
  template: `
    <h3>Practice standalone directive API</h3>

    <p>Host component uses directives the old way</p>
    <app-hello-username-without-api appFontSize
      appBackgroundColor appHoverBlock
      [username]="'John Doe'" [size]="size" [bgColor]="bgColor">
    </app-hello-username-without-api>

    <div>
      <label for="size">
        <span>Size:</span>
        <input id="size" name="size" type="number"
          [(ngModel)]="size"  
          [min]="minSize"
          [max]="maxSize"
        >
      </label>
    </div>

    <div>
      <label for="bgColor">
        <span>Background Color:</span>
        <select id="bgColor" name="bgColor" type="text"
          [(ngModel)]="bgColor"
        >
          <option value="">Please select a color</option>
          <option value="red">Red</option>
          <option value="yellow">Yellow</option>
          <option value="cyan">Cyan</option>
          <option value="green">Green</option>
          <option value="magenta">Magenta</option>
        </select>
      </label>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
  size = 12;
  bgColor = 'yellow';

  minSize= 12;
  maxSize= 64;
}
```

I include the component and directives to the `imports` array, and specify the directive names (`appFontSize`, `appBackgroundColor` and `appHoverBlock`) and the directive inputs (`size` and `bgColor`) on `<app-hello-username-without-api>`.

One component does not seem a lot of work.  However, copy-and-paste becomes tedious when many components use the same directives to modify styles.

```html
<app-component-1 appFontSize appBackgroundColor appHoverBlock [size]="size" [bgColor]="bgColor"></app-component-1>
....
<app-component-n appFontSize appBackgroundColor appHoverBlock [size]="size" [bgColor]="bgColor"></app-component-n>
```

Directive Composition API is designed to eliminate the tedious work and pass data to  the directive inputs as if they are part of the host component.  Let's see how Directive Composition API can power up host component. 

###Power up host component by directive composition API 

```typescript
// hello-username.component.ts

import { ChangeDetectionStrategy, Component, Input  } from '@angular/core';
import { FontSizeDirective } from '../directives/font-size.directive';
import { BackgroundColorDirective } from '../directives/background-color.directive';
import { HoverBlockDirective } from '../directives/hover-block.directive';

@Component({
  selector: 'app-hello-username',
  standalone: true,
  hostDirectives: [
    {
      directive: FontSizeDirective,
      inputs: ['size'],
    },
    {
      directive: BackgroundColorDirective,
      inputs: ['bgColor'],
    },
    HoverBlockDirective,
  ],
  template: `
    <p>Hello {{username}}!!! Hover me to bold text</p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HelloUsernameComponent {
  @Input({ required: true })
  username!: string;
}
```

First, I use `hostDirectives` property to register the standalone directives.  `FontSizeDirective` has `size` input; therefore, I must provide it in the `inputs` array. Similarly, `BackgroundColorDirective` has `bgColor` input and it is included in `inputs` array.  

```typescript
hostDirectives: [
    {
      directive: FontSizeDirective,
      inputs: ['size'],
    },
    {
      directive: BackgroundColorDirective,
      inputs: ['bgColor'],
    },
    HoverBlockDirective,
],
```

When I render `HelloUsernameComponent` in `AppComponent`, I can omit the directive names. 

```html
<p>Host component that host directives and inputs</p>
<app-hello-username [username]="'John Doe'" [bgColor]="bgColor" [size]="size">
</app-hello-username>

<p>Host component that uses host directives and default values</p>
<app-hello-username [username]="'John Doe'"></app-hello-username>
```

The first instance of `<app-hello-username>` changes font size and background color when form values update. The second instance of `<app-hello-username>` uses the default font size and background color in the standalone directives respectively.

###Power up host component by composite directive

We can also use the API to compose directive from standalone directives to combine their capabilities. 

```typescript
// background-block.directive.ts

import { Directive, Input } from '@angular/core';
import { BackgroundColorDirective } from './background-color.directive';
import { FontSizeDirective } from './font-size.directive';
import { HoverBlockDirective } from './hover-block.directive';

@Directive({
  selector: '[appBackgroundBlock]',
  standalone: true,
  hostDirectives: [
    {
      directive: FontSizeDirective,
      inputs: ['size'],
    },
    {
      directive: BackgroundColorDirective,
      inputs: ['bgColor:backgroundColor'],
    },
    HoverBlockDirective,
  ]
})
export class BackgroundBlockDirective {
  @Input()
  size!: string;

  @Input()
  bgColor!: string;
}
```

`BackgroundColorDirective` is consisted of `FontSizeDirective`, `BackgroundColorDirective` and `HoverBlockDirective` directives. Moreover, I rename `bgColor` to `backgroundColor` for demo purpose.

Now, I can register `BackgroundBlockDirective` in the `hostDirectives` array of other host component.

```typescript
// hello-background-block.component.ts

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { BackgroundBlockDirective } from '../directives/background-block.directive';

@Component({
  selector: 'app-hello-background-block',
  standalone: true,
  hostDirectives: [
    BackgroundBlockDirective
  ],
  template: `
    <p>I use BackgroundBlockDirective that is composed of 3 other directives!!!</p>
  `,
  styles: [`
    :host {
      display: block;
    }

    p {
      padding: 0.5rem;
    }
  `],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HelloBackgroundBlockComponent {}
```

In App component, I can render `HelloBackgroundBlockComponent` and it should exhibit the same behavior as `HelloUserNameComponent`

```html
<p>Host component that uses composite host directive</p>
<app-hello-background-block [backgroundColor]="bgColor" [size]="size"></app-hello-background-block>

<p>Host component that uses composite host directive and default values</p>
<app-hello-background-block></app-hello-background-block>
```

The first instance of `<app-hello-background-block>` changes font size and background color when form values update. The second instance of `<app-hello-background-block>` uses the default font size and background color in the standalone directives respectively.

The following Stackblitz repo shows the final results:
{%embed https://stackblitz.com/edit/stackblitz-starters-uqwviz?file=src%2Fmain.ts %}

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

##Resources:

- Github Repo: https://github.com/railsstudent/ng-directive-composition-api-demo
- Stackblitz: https://stackblitz.com/edit/stackblitz-starters-uqwviz?file=src%2Fmain.ts
- Directive Composition API: https://angular.io/guide/directive-composition-api