Angular Signal Inputs: road to Signal Components
Tags: angular, frontend, webdev, javascript
Published: 2024-04-06 18:52:17

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
](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0umog5zwxp7pxdly0ayj.png)

---

## 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:

```ts
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:

```ts
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:

```ts
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:

```ts
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:

```ts
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:

```ts
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:

```ts
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:

```ts
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:

```ts
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:

```ts
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: 👇🏼

{% embed https://dev.to/davidepassafaro/angular-model-inputs-two-way-binding-inputs-with-signals-2nc7 %}

---

## 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](https://www.linkedin.com/in/davide-passafaro/)**. 👋