Examples of new signal inputs in Angular
Tags: tutorial, typescript, angular, webdev
Published: 2024-01-17 04:13:59

##Introduction

In this blog post, I would like to show a new feature of Angular 17.1.0 that is called Signal input. New signal input is important in Angular because it can do things that previous version cannot. For example, signal input facilitates construction of computed signals. Signal input also allows API to use it as path/query parameter in effect to set writable signals.

In this blog post, I am going to show the following practical examples using Pokemon API

- required signal input
- computed signals based on signal inputs
- signal input with initial value
- transformed signal input
- call signal input + API in effect()
- signal input + withComponentInputBinding
- signal input + computed signals + host property in directive
- signal input + RxJS interop

###Install new Angular dependencies

```bash
"@angular/core": "17.1.0-rc.0",
"@angular/forms": "17.1.0-rc.0",
"@angular/common": "17.1.0-rc.0",
"@angular/router": "17.1.0-rc.0",
"@angular/compiler": "17.1.0-rc.0",
"@angular/animations": "17.1.0-rc.0",
"@angular/platform-browser": "17.1.0-rc.0"
```

In package.json of the Stackblitz demo, I update Angular dependencies to 17.1.0-rc.0.

###Required signal input

```typescript
// pokemon.component.ts

import { ChangeDetectionStrategy, Component, input } from '@angular/core';

@Component({
  selector: 'app-pokemon',
  standalone: true,
  imports: [PokemonCardComponent],
  ],
  template: `
    <p>Pokemon id: {{ id() }}</p>
    <hr />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent {
  id = input.required<number>();
}
```

`PokemonComponent` has a required signal input, id, that expects a number. This numerical value is then displayed in inline template.

```typescript
// main.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [PokemonComponent, RouterOutlet, RouterLink, FormsModule],
  template: `
    <div>
      <app-pokemon [id]="25"  />
      <app-pokemon [id]="52" />
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class App {}
```

In App component, I import `PokemonComponent` and pass number to pokemon id to the required signal input

###Computed signal based on signal input

```typescript
// pokemon.componen.ts

import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';

@Component({
  selector: 'app-pokemon',
  standalone: true,
  template: `
    <p>Pokemon id: {{ id() }}, Next Pokemon id: {{ nextId() }}</p>
    <hr />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent {
  id = input.required<number>();
  nextId = computed(() => this.id() + 1);
}
```

`nextId` is a computed signal that increments id signal input by 1.  When id is 25, `nextId` signal holds 26. When id is 52, `nextId` signal holds 53. In the inline template, `id` signal input and `nextId` computed signal are displayed.

###Signal Input with initial value

```typescript
// pokemon.componen.ts

import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';

@Component({
  selector: 'app-pokemon',
  standalone: true,
  template: `
    <p>Pokemon id: {{ id() }}, Next Pokemon id: {{ nextId() }}</p>
    <p [style.background]="bgColor()">
      Background color: {{ bgColor() }}
    </p>
    <hr />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent {
  id = input.required<number>();
  bgColor = input('cyan', { alias: 'backgroundColor' });

  nextId = computed(() => this.id() + 1);
}
```

In the component, I declare a signal input with initial value, cyan, and is given an alias `backgroundColor`.  When the component does not have `backgroundColor` input, the value becomes cyan. Otherwise, the value of `backgroundColor` is overwritten. The inline template displays the result of `bgColor()` and uses the same result to change the background colour of the paragraph.

```typescript
// main.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [PokemonComponent, RouterOutlet, RouterLink, FormsModule],
  template: `
    <div>
      <app-pokemon [id]="25"  />
      <app-pokemon [id]="52" backgroundColor="yellow" />
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class App {}
```

###Transformed Signal Input

```typescript
// pokemon.componen.ts

import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';

@Component({
  selector: 'app-pokemon',
  standalone: true,
  template: `
    <p>Pokemon id: {{ id() }}, Next Pokemon id: {{ nextId() }}</p>
    <p [style.background]="bgColor()">
      Background color: {{ bgColor() }}
    </p>
    <p>Transformed: {{ text() }}</p>
    <hr />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent {
  id = input.required<number>();
  bgColor = input('cyan', { alias: 'backgroundColor' });
  text = input<string, string>('', {
    alias: 'transformedText',
    transform: (v) => `transformed ${v}!`,
  });

  nextId = computed(() => this.id() + 1);
}
```

In the component, I declare another signal input with an empty string, and is given an alias `transformedText` and a transform function.  The transform function accepts the string, prepends 'transformed' and appends an exclamation mark to it. Similarly,  the inline template displays the result of `text()` that is the value after the transformation.

```typescript
// main.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [PokemonComponent, RouterOutlet, RouterLink, FormsModule],
  template: `
    <div>
      <app-pokemon [id]="25"  transformedText="red" />
      <app-pokemon [id]="52" backgroundColor="yellow" transformedText="green" />
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class App {}
```

The first PokemonComponent displays "transformed red!" while the second PokemonComponent displays "transformed green!".

The next three examples demonstrates advanced usage of signal input

###Call signal input + API in effect()

```typescript
// get-pokemon.util.ts

import { HttpClient } from '@angular/common/http';
import { assertInInjectionContext, inject } from '@angular/core';
import { PokemonType } from '../types/pokemon.type';

export function getPokemonFn() {
    assertInInjectionContext(getPokemonFn);
    const httpClient = inject(HttpClient);
    const URL = `https://pokeapi.co/api/v2/pokemon`;
  
    return function (id: number) {
      return httpClient.get<PokemonType>(`${URL}/${id}/`)
    }
}
```

```typescript
// pokemon.componen.ts

@Component({
  selector: 'app-pokemon',
  standalone: true,
  imports: [PokemonCardComponent],
  template: `
     ... omitted unrelated codes ...

     <div class="container">
       @if (pokemon(); as pokemon) {
           <app-pokemon-card [pokemon]="pokemon" />
        }

        @if (nextPokemon(); as pokemon) {
             <app-pokemon-card [pokemon]="pokemon" />
         }
    </div>
    <hr />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent {
  id = input.required<number>(); 
  nextId = computed(() => this.id() + 1);
  getPokemon = getPokemonFn();

  pokemon = signal<PokemonType | undefined>(undefined);
  nextPokemon = signal<PokemonType | undefined>(undefined);

  constructor() {
    effect((onCleanup) => {
      const sub = this.getPokemon(this.id())
        .subscribe((pokemon) => this.pokemon.set(pokemon));
      const sub2 = this.getPokemon(this.nextId())
        .subscribe((pokemon) => this.nextPokemon.set(pokemon));
 
      onCleanup(() => {
        sub.unsubscribe();
        sub2.unsubscribe();
      });
    });
  }
}
```

In `effect()`, the callback function invokes `this.getPokemon` function to retrieve Pokemon by `id` signal input and `nextId` computed signal respectively.  When the first Observable subscribes, the Pokemon object is set to `pokemon` signal.  When the second Observable subscribes, the Pokemon object is set to `nextPokemon` signal.  Moreover, the `onCleanup` function unsubscribes the subscriptions to avoid memory leak.  In the inline template, the new control flow tests the signals are defined before passing the data to `PokemonCardComponent` to handle the rendering.

###signal input + withComponentInputBinding

```typescript
// app.config.ts

import { provideHttpClient } from '@angular/common/http';
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    provideRouter(routes, withComponentInputBinding())
  ]
};
```

`provideRouter` has `withComponentInputBinding` feature; therefore,  route data is easily converted to signal input in component.

```typescript
// app.route.ts

import { Routes } from '@angular/router';

export const routes: Routes = [
    {
        path: 'pokemons/pidgeotto',
        loadComponent: () => import('./pokemons/pokemon/pokemon.component').then((m) => m.PokemonComponent),
        data: {
            id: 17,
            backgroundColor: 'magenta',
            transformedText: 'magenta',
        }
    },
];
```

In 'pokemons/pidgeotto' route path, I supply route data, `id`, `backgroundColor` and `tranformedText`.  Since `withComponentInputBinding` is enabled, the route data automatically converts to `id`, `bgColor` and `text` signal inputs.

```typescript
// main.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [PokemonComponent, RouterOutlet, RouterLink, FormsModule],
  template: `
    <div>
    <h2>Signal inputs with route data</h2>
    <ul>
         <li><a [routerLink]="['/pokemons/pidgeotto']">pidgeotto</a></li>
    </ul>
    <router-outlet />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class App {}
```

When user clicks on the hyperlink, user is routed to PokemonComponent to retrieve pidgeotto and pidgeot from the Pokemon API.

###signal input + computed signals + host property in directive

```typescript
// font-size.directive.ts

import { computed, Directive, input } from '@angular/core';

@Directive({
  selector: '[appFontSize]',
  standalone: true,
  host: {
    '[style.font-size.px]': 'size()',
    '[style.font-weight]': 'fontWeight()',
    '[style.font-style]': 'fontStyle()',
    '[style.color]': 'color()'
  },
})
export class FontSizeDirective {
  size = input(14);
  shouldDoStyling = computed(() => this.size() > 20 && this.size() <= 36);
  fontWeight = computed(() => this.shouldDoStyling() ? 'bold' : 'normal');
  fontStyle = computed(() => this.shouldDoStyling() ? 'italic' : 'normal');
  color = computed(() => this.shouldDoStyling() ? 'blue' : 'black');
}
```

I create a font size directive to update the font size, font weight, font style and color of HTML DOM elements. `size` is a signal input with an initial value 14.  Whenever size updates, `shouldDoStyling` determines whether or not CSS styling should apply.  

- fontWeight - a computed signal that returns either bold or normal
- fontStyle - a computed signal that returns italic or normal
- color  - a computed signal that returns blue or black

```typescript
// pokemon.component.ts

@Component({
  selector: 'app-pokemon',
  standalone: true,
  imports: [PokemonCardComponent],
  hostDirectives: [
    {
      directive: FontSizeDirective,
      inputs: ['size'],
    }
  ],
  template: `
    <p>Pokemon id: {{ id() }}, Next Pokemon id: {{ nextId() }}</p>
    <p [style.background]="bgColor()">
      Background color: {{ bgColor() }}
    </p>
    <p>Transformed: {{ text() }}</p>

     // omitted because the code is irrelevant
    <hr />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent {
  id = input.required<number>();
  bgColor = input('cyan', { alias: 'backgroundColor' });
  text = input<string, string>('', {
    alias: 'transformedText',
    transform: (v) => `transformed ${v}!`,
  });

  nextId = computed(() => this.id() + 1);

  // omitted because the code is irrelevant
}
```

`PokemonComponent` registers `FontSizeDirective` as a host directive with a size input.  CSS styling applies to the paragraph elements when App component assigns a value to the size input. 

```typescript
// main.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [PokemonComponent, RouterOutlet, RouterLink, FormsModule],
  template: `
    <div>
      <label for="size">
        <span>Size: </span>
        <input type="number" id="size" name="size" [ngModel]="size()" (ngModelChange)="size.set($event)" min="8" />
      </label>
      <app-pokemon [id]="25" transformedText="red" [size]="size()" />
      <app-pokemon [id]="52" backgroundColor="yellow" transformedText="green" [size]="size()" />
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class App {
  size = signal(16);
}
```

App component has a template-driven form with a number field. When user inputs a new size, the size input of  PokemonComponent affects the styling of the paragraph elements.

There is one example left that is the signal input and RxJS interop.

```typescript
// pokemon.component.ts

@Component({
  selector: 'app-pokemon',
  standalone: true,
  imports: [PokemonCardComponent, AsyncPipe],
  hostDirectives: [
    {
      directive: FontSizeDirective,
      inputs: ['size'],
    }
  ],
  template: `
    <p>Observable: {{ value$ | async }}</p>
    <hr />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent {
  id = input.required<number>();
  bgColor = input('cyan', { alias: 'backgroundColor' });
  text = input<string, string>('', {
    alias: 'transformedText',
    transform: (v) => `transformed ${v}!`,
  });
  
  value$ = toObservable(this.bgColor)
    .pipe(
      combineLatestWith(toObservable(this.text)),
      map(([color, color2]) => `${color}|${color2}`),
      map((color) => `@@${color.toUpperCase()}@@`),
    );
}
```

`value$` is an Observable that combines `toObservable(this.bgColor)` and `toObservable(this.text)` to transform the signal inputs into a new text.  In the inline template, I use the async pipe to resolve `value$` and display its final value

These are all the practical examples that can achieve using the new signal input in Angular 17.1.0.

The following Stackblitz repo shows the final results:
{% embed https://stackblitz.com/edit/stackblitz-starters-i3febt?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-signal-input-demo
- Github Page: https://railsstudent.github.io/ng-signal-input-demo/
- Stackblitz Demo: https://stackblitz.com/edit/stackblitz-starters-i3febt?file=src%2Fmain.ts
- Revolutionizing Angular: Introducing the New Signal Input API:  https://netbasal.com/revolutionizing-angular-introducing-the-new-signal-input-api-d0fc3c8777f2
- Angular Signal Inputs are here to change the game: https://itnext.io/angular-signal-inputs-are-here-to-change-the-game-d0ec967b301f