HostAttributeToken – Injection token of static host attribute in Angular

Reading Time: 5 minutes

Loading

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,

<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 to inject static attributes to follow the wave of Signal evolution in Angular 17.3.0.

In this post, I am going to provide four examples that illustrate the usage of the HostAttributeToken.

  • Inject static attributes at 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 host directive can inject the static attributes of the host
let's go

Use case 1: Inject HostAttributeToken in a component

// 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. If the attribute does not exist on the host node, the function will return the default value.

// 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, fontSize, color, and background. Then, I declared a computed signal, styles, to create an object of CSS styles. Next, I assigned style() to the ngStyle attribute directive of the paragraph element.

// main.ts

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

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

// 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

// 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.

// 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.

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

The paragraph element 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 3: Inject HostAttributeToken to update classes in a directive

// 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.

// 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 type static attribute and the default value is primary.

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

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

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

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

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

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

Use case 3: Inject HostAttributeToken in a composite directive

// 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.

// 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 600, a font size of 32, purple text, and yellow background.

<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 attribute name does not exists, @Attribute will return null. However, HostAttributeToken returns error when the host does not provide the attribute name. It is good practice to pass { option: true } to the inject function and also a default value.

The following Stackblitz repo displays the final results:

This is the end of the blog post that analyzes data retrieval patterns in Angular. I hope you like the content and continue to follow my learning experience in Angular, NestJS and other technologies.

Resources:

  1. Stackblitz Demo: https://stackblitz.com/edit/stackblitz-starters-dghpis?file=src%2Fmain.ts
  2. HostAttributeToken: https://angular.io/api/core/HostAttributeToken