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
"@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
// 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.
// 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
// 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
// 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.
// 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
// 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.
// 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()
// 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}/`)
}
}
// 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
// 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.
// 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.
// 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
// 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
// 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.
// 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.
// 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:
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