L’introduzione dei Signal Inputs è il primo passo verso i Signal Components e le applicazioni Angular zoneless, introduzione che migliora fin da subito la qualità del nostro codice e l’esperienza di sviluppo. In questo articolo vedremo come utilizzarli.
⚠️ ATTENZIONE: i Signal Inputs sono ancora ufficialmente in developer preview ⚠️
Ciao ciao @Input( ); Benvenuta funzione input( )
Creare un Signal Input è piuttosto semplice:
invece di creare un input utilizzando il decoratore @Input( ), dobbiamo utilizzare la funzione input( ) fornita da @angular/core.
Ecco un esempio di come possiamo creare un input di tipo stringa:
import { Component, input } from '@angular/core';
@Component({ ... })
export class MyComponent {
myProp = input<string>();
}
Code language: TypeScript (typescript)
Utilizzando la funzione input( ) i nostri input vengono creati di tipo InputSignal, uno speciale tipo di Signal read-only definito cosi:
* Un InputSignal è simile ad un signal non-writable tranne che porta con se informazioni addizionali sulla tipizzazione per la proprietà transform, e che Angular aggiorna internamente il signal quando un nuovo valore viene fornito.
Più precisamente i nostri Signal Inputs sono di questo tipo:
myProp: InputSignal<ReadT, WriteT = ReadT> = input<ReadT>(...)
Code language: TypeScript (typescript)
Dove ReadT rappresenta il tipo del valore del signal e WriteT invece il tipo del valore atteso dal componente padre.
Seppure questi due tipi spesso coincidono, approfondiremo il loro ruolo e le differenze tra essi quando parleremo della funzione transform più avanti.
Torniamo ora all’esempio precedente concentrandoci sul tipo del nostro input:
import { Component, InputSignal, input } from '@angular/core';
@Component({ ... })
export class MyComponent {
myProp: InputSignal<string | undefined, string | undefined> = input<string>();
}
Code language: TypeScript (typescript)
Quegli undefined sono risultanti dalla natura opzionale del valore dell’input.
Per specificare che un input è required, e sbarazzarci quindi di quei fastidiosi undefined, la nuova input api ci offre una apposita funzione required( ):
import { Component, InputSignal, input } from '@angular/core';
@Component({ ... })
export class MyComponent {
myProp: InputSignal<string, string> = input.required<string>();
}
Code language: TypeScript (typescript)
In alternativa, possiamo fornire un valore di default alla funzione input( ):
import { Component, InputSignal, input } from '@angular/core';
@Component({ ... })
export class MyComponent {
myProp: InputSignal<string, string> = input<string>('');
}
Code language: TypeScript (typescript)
Allo stesso modo, il valore di default può essere fornito anche alla funzione required( ).
Leggi anche: Angular Control Flow, la guida completa
Niente più ngOnChanges( )
Al giorno d’oggi è molto comune utilizzare ngOnChanges o funzioni setter per svolgere azioni quando un determinato input viene aggiornato.
Con i Signal Inputs, possiamo sfruttare a pieno la potenza dei Signals e sbarazzarci di queste funzioni grazie a due funzionalità: computed ed effect.
Computed Signals
Utilizzando la funzione computed possiamo definire valori derivati a partire da altri input, uno o più, che saranno sempre sincronizzati mano a mano che i valori vengono aggiornati:
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)
Dunque ogni volta che l’input description viene modificato, dall’interno e dall’esterno, il valore di descriptionLength viene ricalcolato e aggiornato di conseguenza.
Effect
Con la funzione effect, possiamo invece definire side effects che vengano eseguiti quando i nostri input, uno o più, vengono aggiornati.
Per esempio, immaginiamo di dover aggiornare una entità proveniente da uno script third-party, che stiamo usando per creare un grafico a barre, quando un input viene aggiornato:
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)
Oppure di dover effettuare una richiesta http:
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)
Utilizzare computed ed effect rende i nostri componenti più robusti e ottimizzati, migliorando inoltre di molto la manutenibilità del nostro codice.
Alias e funzione transform
Per garantire una migrazione degli input creati con @Input( ) più semplice, i Signal Inputs supportano anche la proprietà alias e la funzione transform:
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 particolare, grazie alla funzione transform possiamo definire una funzione per manipolare il valore del nostro input prima che sia disponibile all’interno del componente, ed è proprio qui che entrano in gioco ReadT e WriteT.
Infatti, utilizzare la funzione transform può portare ad una differenza tra il tipo del valore fornito dal componente padre, rappresentato WriteT, e il tipo del valore all’interno del nostro Signal Input, rappresentato da ReadT.
Per questo motivo, quando creiamo un Signal Input con la funzione transform possiamo specificare entrambi i valori di ReadT e WriteT come type arguments della funzione:
mySimpleProp: InputSignal<ReadT, WriteT = ReadT> = input<ReadT>(...)
myTransformedProp: InputSignal<ReadT, WriteT> = input<ReadT, WriteT>( ... , {
transform: transformFunction
});
Code language: TypeScript (typescript)
Senza la funzione transform il valore di WriteT è valorizzato identico a ReadT, mentre utilizzando la funzione transform sia ReadT che WriteT sono definiti distintamente.
E per quanto riguarda il two-way binding?
Al momento non è possibile implementare il two-way binding con i Signal Inputs, ma in futuro verrà esposta una nuova api chiamata Model Input che fornirà una funzione set( ).
Stay tuned!!!
Grazie per aver letto questo articolo 🙏
Mi piacerebbe avere qualche feedback quindi grazie in anticipo per qualsiasi commento. 👋
Un ringraziamento speciale a Paul Gschwendtner ed a tutto il Team Angular per questa funzionalità.