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
// 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
// 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
.
// 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.
<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
// 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.
hostDirectives: [
{
directive: FontSizeDirective,
inputs: ['size'],
},
{
directive: BackgroundColorDirective,
inputs: ['bgColor'],
},
HoverBlockDirective,
],
When I render HelloUsernameComponent
in AppComponent
, I can omit the directive names.
<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.
// 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.
// 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
<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:
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