Signal Inputs introduction is the initial act of the upcoming rise of Signal Components and zoneless Angular applications, enhancing already both code quality and developer experience. Let’s delve into how they work.
⚠️ ALERT: new Signal Inputs are still in developer preview ⚠️
Bye @Input( ) decorator; Welcome input( ) function
Creating a Signal Input is quite simple:
rather than creating an input using the @Input( ) decorator, you should now use the input( ) function provided by @angular/core.
Let’s see an example of creating an input of string type:
import { Component, input } from '@angular/core';
@Component({ ... })
export class MyComponent {
myProp = input<string>();
}
Code language: TypeScript (typescript)
Using the input( ) function your inputs will be typed as InputSignal, a special type of read-only Signal defined as follows:
* An InputSignal is similar to a non-writable signal except that it also carries additional type-information for transforms, and that Angular internally updates the signal whenever a new value is bound.
More specifically your Signal Inputs will be typed as the following:
myProp: InputSignal<ReadT, WriteT = ReadT> = input<ReadT>(...)
Code language: TypeScript (typescript)
Where ReadT represents the type of the signal value and WriteT represents the type of the expected value from the parent.
Although these types are often the same, I’ll delve deeper into their role and differences discussing the transform function later on.
Let’s go back to the previous example focusing on the input value type:
import { Component, InputSignal, input } from '@angular/core';
@Component({ ... })
export class MyComponent {
myProp: InputSignal<string | undefined, string | undefined> = input<string>();
}
Code language: TypeScript (typescript)
Those undefined are given by the optional nature of the input value.
To define your input as required, and thus get rid of those nasty undefined, the input api offers a dedicated required( ) function:
import { Component, InputSignal, input } from '@angular/core';
@Component({ ... })
export class MyComponent {
myProp: InputSignal<string, string> = input.required<string>();
}
Code language: TypeScript (typescript)
Alternatively, you can provide a default value to the input( ) function:
import { Component, InputSignal, input } from '@angular/core';
@Component({ ... })
export class MyComponent {
myProp: InputSignal<string, string> = input<string>('');
}
Code language: TypeScript (typescript)
The default value can be provided to the required( ) as well.
Read also: Angular Control Flow, the Complete Guide
No more ngOnChanges( )
Nowadays you typically use ngOnChanges and setter functions to perform actions when an input is updated.
With Signal Inputs, you can take advantage of the great flexibility of Signals to get rid of those functions using two powerful tools: computed and effect.
Computed Signals
Using computed you can easily define derived values starting from your inputs, one or more, that will be always updated based on the latest values:
import { Component, InputSignal, Signal, computed, input } from '@angular/core';
@Component({ ... })
export class MyComponent {
description: InputSignal<string, string> = input<string>('');
descriptionLength: Signal<number> = computed(() => this.description.length);
}
Code language: TypeScript (typescript)
So each time the description value is modified, the value of descriptionLength is recalculated and updated accordingly.
Effect
With effect, you can define side effects to run when your inputs, one or more, are updated.
For example, imagine you need to update a third-party script you are using to build your chart component when an input is updated:
import Chart from 'third-party-charts';
import { effect, Component, InputSignal, input } from '@angular/core';
@Component({ ... })
export class MyComponent {
chartData: InputSignal<string[], string[]> = input.required<string[]>();
constructor() {
const chart = new Chart({ ... });
effect((onCleanup) => {
chart.updateData(this.chartData());
onCleanup(() => {
chart.destroy();
});
});
}
}
Code language: TypeScript (typescript)
Or even perform an http request:
import { HttpClient } from '@angular/common/http';
import { effect, Component, InputSignal, inject, input } from '@angular/core';
@Component({ ... })
export class MyComponent {
myId: InputSignal<string, string> = input.required<string>();
response: string = '';
constructor() {
const httpClient = inject(HttpClient);
effect((onCleanup) => {
const sub = httpClient.get<string>(`myurl/${this.myId()}/`)
.subscribe((resp) => { this.response = resp });
onCleanup(() => {
sub.unsubscribe();
});
});
}
}
Code language: TypeScript (typescript)
Using computed and effect will make your components more robust and optimized, enhancing also a lot the maintainability of the code.
Alias and transform function
In order to guarantee a smoother migration from decorator-based inputs, Signal Inputs supports also alias and transform function properties:
import { HttpClient } from '@angular/common/http';
import { effect, Component, InputSignal, inject, input } from '@angular/core';
@Component({ ... })
export class MyComponent {
textLength: InputSignal<number, string> = input<number, string>(0, {
alias: 'descriptionText',
transform: (text) => text.length
});
}
Code language: TypeScript (typescript)
In particular, thanks to the transform function you can define a function that manipulates your input before it is available in the component scope and this is where the difference between ReadT and WriteT comes into play.
In fact, using the transform function can create a mismatch between the type of the value being set from the parent, represented by WriteT, and the type of the value stored inside your Signal Input, represented by ReadT.
For this reason, when creating a Signal Input with the transform function you can specify both ReadT and WriteT as the function type arguments:
mySimpleProp: InputSignal<ReadT, WriteT = ReadT> = input<ReadT>(...)
myTransformedProp: InputSignal<ReadT, WriteT> = input<ReadT, WriteT>( ... , {
transform: transformFunction
});
Code language: TypeScript (typescript)
As you can see, without the transform function the value of WriteT is set as identical to ReadT, while using the transform function both ReadT and WriteT are defined distinctly.
What about two-way binding?
Currently, there is no way to implement two-way binding with Signal Inputs, but in the future, there will be an api called Model Input that will expose a set( ) function to fulfill this behavior.
So stay tuned!!!
Thanks for reading so far 🙏
I’d like to have your feedback so please feel free to contact me for any. 👋
Special thanks to Paul Gschwendtner and to the Angular Team for this feature.