HostAttributeToken - Injection token of static host attribute in Angular
Tags: angular, tutorial, webdev
Published: 2024-04-01 08:28:49

##Introduction

`HostAttributeToken` is a new feature in Angular 17.3.0 that injects static attributes of the host node. The decorator version of `HostAttributeToken` is `@Attribute`, and it is recommended to use it over @Input because Input triggers change detection even when the input value is static.

Before Angular 17.3.0, 

```typescript
<app-comp  hello='A' />

@Component({
   selector: 'app-comp,
   template: `<div>{{hello}}</div>`
})
export class SomeComponent {
    hello: string;

    constructor(@Attribute('hello) hello: string) {
        this.hello = hello;   // A
    }
} 
```

`HostAttributeToken` is an injection token that injects static attributes to follow the wave of signal evolution in Angular 17.3.0.

In this post, I will provide four examples that illustrate the usage of the HostAttributeToken.  

- Inject static attributes at the component level
- In a directive,  inject static attributes to update CSS styles
- In a directive, inject static attributes to toggle CSS classes
- Create a composite directive where the host directive can inject the static attributes of the host

###Use case 1: Inject HostAttributeToken in a component

```typescript
// host-attr-token.util.ts

import { HostAttributeToken, assertInInjectionContext, inject } from "@angular/core";

export function injectHostAttrToken<T = string>(key: string, defaultValue: T) {
  assertInInjectionContext(injectHostAttrToken);

  const token = new HostAttributeToken(key);
  return (inject(token, { optional: true }) || defaultValue) as T;    
}
```

I created an `injectHostAttrToken` utility function that accepts a key and a default value to inject the static attribute on the host node. The function will return the default value if the attribute does not exist on the host node.

```typescript
// host-attr-styles.component.ts

import { NgStyle } from "@angular/common";
import { ChangeDetectionStrategy, Component, computed } from "@angular/core";
import { injectHostAttrToken } from "../host-attr-token.util";

@Component({
  selector: 'app-host-attr',
  imports: [NgStyle],
  standalone: true,
  template: `
    <p>Inject Host Attribute Token in a component</p>
    <p [ngStyle]="styles()">
      Style this element
    </p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HostAttrStylesComponent {
  padding = injectHostAttrToken('padding', '0.5rem');  
  fontSize = injectHostAttrToken('font-size', '18');  
  color = injectHostAttrToken('color', 'yellow');
  background = injectHostAttrToken('backgroundColor', '#abc');

  styles = computed(() => ({
    padding: this.padding,
    'fontSize.px': this.fontSize,
    color: this.color,
    background: this.background,
  }));
}
```

`HostAttrStylesComponent` uses `injectHostAttrToken` to inject padding, font size, color, and background. Then, I declared a computed signal, styles, to create an object of CSS styles. Next, I assigned style() to the paragraph element's `ngStyle` attribute directive.

```typescript
// main.ts

<app-host-attr padding='1rem' font-size='28' color='orange' backgroundColor='rebeccapurple' />
```

The component injects the static attribute values and updates the CSS of padding, font size, color, and background color.

```typescript
// main.ts

<app-host-attr />
```

The component does not provide any attribute, and default values are used. The padding, font size, color, and background color are 0.5rem, 18px, yellow, and #abc, respectively.

###Use case 2: Inject HostAttributeToken to update styles in a directive

```typescript
// host-attr-styles.attr.ts

import { Directive } from "@angular/core";
import { injectHostAttrToken } from "../host-attr-token.util";

@Directive({
  selector: '[app-host-attr-styles]',
  standalone: true,
  host: {
    "[style.background]": 'background',
    "[style.padding]": 'padding',
    "[style.fontSize.px]": 'fontSize',
    "[style.color]": 'color',
  }
})
export class HostAttrStylesDirective {
  padding = injectHostAttrToken('padding', '0.5rem');  
  fontSize = injectHostAttrToken('font-size', '18');  
  color = injectHostAttrToken('color', 'yellow');
  background = injectHostAttrToken('backgroundColor', '#abc');
}
```

I can also inject static attributes in a directive and apply the directive to HTML elements.

```typescript
// main.ts

<p app-host-attr-styles padding='1.25rem' font-size='20' 
      color='yellow' backgroundColor='red'>
      Style by Host Attr Styles Directive
</p>
```

The directive injects the static attributes and styles the paragraph element. The padding, font size, color, and background color are 1.25rem, 20px, yellow, and red, respectively.

```typescript
// main.ts

<p app-host-attr-styles>Style by Host Attr Styles Directive</p>
```

The paragraph element provides no attribute, and default values are used. The padding, font size, color, and background color are 0.5rem, 18px, yellow, and #abc, respectively.

###Use case 3: Inject HostAttributeToken to update classes in a directive

```css
// global_styles.css

.primary {
  background-color: #a5c2e0;
  color: #6c3183;
  font-size: 1.5rem;
  padding: 1rem;
}

.secondary {
  background-color: #69d897; 
  color: rgb(91, 89, 209);
  font-size: 1.5rem;
  padding: 0.75rem;
}
```

In this use case, I defined two global styles to apply to the HTML elements in the AppComponent.

```typescript
// host-attr-class.directive.ts

import { Directive } from '@angular/core';
import { injectHostAttrToken } from '../host-attr-token.util';

@Directive({
  selector: '[app-host-attr-class]',
  standalone: true,
  host: {
    '[class.primary]': 'isPrimary',
    '[class.secondary]': 'isSecondary',
  },
})
export class HostAttrClassDirective {
  type = injectHostAttrToken('type', 'primary');
  isPrimary = this.type === 'primary';
  isSecondary = this.type === 'secondary';
}
```

`HostAttrClassDirective` injects the `type` static attribute, and the default value is primary. 

```html
<p app-host-attr-class type='primary'>Primary class</p>
```

When `type` is primary, the paragraph element uses the primary class for styling. 

```html
<p app-host-attr-class type='secondary'>Secondary class</p>
```

When `type` is secondary, the element uses the secondary class for styling.  

```html
<p app-host-attr-class>Default primary class</p>
```

The paragraph element does not have the `type` attribute; therefore, it uses the primary class for styling.

###Use case 3: Inject HostAttributeToken in a composite directive

```typescript
// host-attr-composition.directive.ts

import { Directive } from "@angular/core";
import { injectHostAttrToken } from "../host-attr-token.util";
import { HostAttrStylesDirective } from "../host-attr-styles-directive/host-attr-styles.directive";

@Directive({
  selector: '[app-host-attr-composition]',
  standalone: true,
  host: {
    "[style.fontWeight]": 'fontWeight',
  },
  hostDirectives: [
    {
      directive: HostAttrStylesDirective
    }
  ]
})
export class HostAttrCompositionDirective {
  fontWeight = injectHostAttrToken('font-weight', 'bold'); 
}
```

`HostAttrCompositionDirective` is a composite directive that registers the host directive, `HostAttrStylesDirective`. This directive leverages the host directive to inject the static attribute values of padding, font size, color, and background color. Moreover, it also injects the static attribute value of font weight with a default value of bold. 

```typescript
// main.ts 

@Component({ 
    ... other properties...
    imports: [HostAttrCompositionDirective]
})

<p app-host-attr-composition padding="1.75rem" 
      font-weight='600' font-size="32" color="rebeccapurple" 
      backgroundColor='yellow'>
      Host Attribute Composition Directive API
 </p>
```

The directive updates the CSS styles of the paragraph element. Now, the element has 1.75rem padding, a font weight of 600, a font size of 32, purple text, and a yellow background.  

```html
<p app-host-attr-composition >
    Default Host Attribute Composition Directive API
</p>
```

In this example, the directive styles the paragraph element with default values.  The padding, font weight, font size, text, and background color are 0.5rem, bold, 18px, rebeccapurple, and yellow, respectively. 

###Rules of thumb

When the value is dynamic, use @Input or signal input. Otherwise, the compiler issues an error.

When the value is static, use @Attribute or HostAttributeToken.  If the attribute name does not exist, @Attribute will return null.  However, HostAttributeToken returns an error when the host does not provide the attribute name. Passing { option: true } to the inject function and a default value is good practice.

The following Stackblitz repo displays the final results:

{%embed https://stackblitz.com/edit/stackblitz-starters-dghpis?file=src%2Fmain.ts %}

I hope you like the content and continue to follow my learning experience in Angular, NestJS, and other technologies.

##Resources:

- Stackblitz Demo: https://stackblitz.com/edit/stackblitz-starters-dghpis?file=src%2Fmain.ts
- Github Repo: https://github.com/railsstudent/ng-host-attribute-token-demo
- HostAttributeToken:  https://angular.io/api/core/HostAttributeToken