DEV Community

Cover image for Angular Signal Inputs: road to Signal Components
Davide Passafaro
Davide Passafaro

Posted on • Edited on • Originally published at codemotion.com

Angular Signal Inputs: road to Signal Components

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


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>();
}
Enter fullscreen mode Exit fullscreen mode

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>(...)
Enter fullscreen mode Exit fullscreen mode

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>();
}
Enter fullscreen mode Exit fullscreen mode

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>();
}
Enter fullscreen mode Exit fullscreen mode

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>('');
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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();
      });
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
      });
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  });
}
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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, like or follow. 👏

And if you really liked it please follow me on LinkedIn. 👋

Top comments (1)

Collapse
 
jangelodev profile image
João Angelo

Hi Davide Passafaro,
Your tips are very useful
Thanks for sharing