Angular Signal Inputs: road to Signal Components

Davide Passafaro
5 min readFeb 7, 2024

--

Angular v17.1.0 has been released recently and it introduced a new amazing input API designed to enable early access to Signal Inputs.

Signal Inputs introduction is the initial act of the upcoming rise of Signal Components and zoneless Angular application, enhancing already both code quality and developer experience. Let’s delve into how they work.

TL;DR: new signal inputs finally arrived in the Angular ecosystem

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 creating an input of string type:

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

@Component({ ... })
export class MyComponent {
myProp = input<string>();
}

Using the input() function your inputs will be typed as InputSignal, a special type of read-only Signal defined as following:

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>(...)

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>();
}

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>();
}

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>('');
}

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);
}

So each time 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();
});
});
}
}

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();
});
});
}
}

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
});
}

In particular, thanks to 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
});

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?

There is no way to implement two-way binding with Signal Inputs, but there is a dedicated API called Model Input that exposes an update() function to fulfill this behavior.

👇🏼 You can find my dedicated article here: 👇🏼

Thanks for reading so far 🙏

I’d like to have your feedback so please leave a comment, clap or follow. 👏
And if you really liked it please follow me on LinkedIn. 👋

--

--

Davide Passafaro

Senior Frontend Engineer 💻📱 | Tech Speaker 🎤 | Angular Rome Community Manager 📣