Angular
Angular Router con Signals: parametri, dati e meno codice da scrivere
Parametri della rotta, query string e dati del progetto direttamente nei signal input. Vediamo insieme come collegarli e usare le computed per costruire una pagina di dettaglio più semplice.
Se lavori con Angular, probabilmente ti sarà capitato di leggere un parametro dalla rotta, usarlo per caricare dei dati e aggiornare il componente quando cambia l’URL. Una cosa normale, certo. Ma a volte, per arrivare a mostrare una semplice pagina di dettaglio, ti ritrovi a scrivere più codice di collegamento che codice per la pagina stessa.
E se potessimo ricevere quei valori direttamente come signal input? Oggi vediamo insieme come usare il Router di Angular con i signals, partendo da un esempio pratico: la pagina di un progetto, con la sua panoramica e le attività da completare.
Partiamo dalla pagina che vogliamo costruire
Immagina di aprire /progetti/demo?tab=attivita. Ci servono tre cose: l’identificativo demo, la scheda da mostrare e i dati del progetto. Non sono tutti la stessa cosa: l’ID sta nel percorso, la scheda è un query parameter e il progetto arriva dal server.
Il componente, però, può riceverli attraverso lo stesso meccanismo: gli input. Il collegamento si basa sui nomi. Il parametro :id arriverà all’input id, ?tab=attivita all’input tab e la risorsa project all’input con quel nome. Ecco il punto da ricordare prima di scrivere codice.
Abilitiamo il collegamento tra Router e input
Non basta dichiarare gli input nel componente. Dobbiamo dire al Router di valorizzarli, aggiungendo withComponentInputBinding() alla configurazione.
Per caricare il progetto useremo anche i Router Resources, disponibili da Angular 22.2 e ancora in developer preview. Puoi usare il binding degli input anche senza questa funzionalità: qui la aggiungiamo per vedere come collegare anche i dati della pagina.
app.config.ts
import type { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import {
provideRouter,
withComponentInputBinding,
withRouterResources,
} from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideRouter(
routes,
withComponentInputBinding(),
withRouterResources(),
),
],
};
provideHttpClient() ci servirà per la richiesta HTTP. Gli altri due ingredienti hanno compiti diversi: uno collega i dati della rotta agli input, l’altro abilita le risorse nella configurazione delle rotte.
Carichiamo il progetto dalla rotta
Prima definiamo i dati che ci aspettiamo dal server. Per questo esempio bastano il nome del progetto e due numeri con cui calcolare l’avanzamento:
project.ts
export interface Project {
id: string;
name: string;
completedTasks: number;
totalTasks: number;
}
Poi aggiungiamo la rotta. L’endpoint /api/projects/:id deve esistere nel tuo backend e restituire un oggetto con questa struttura; il Router non crea l’API al posto nostro.
app.routes.ts
import { httpResource } from '@angular/common/http';
import type { Routes } from '@angular/router';
import type { Project } from './project';
export const routes: Routes = [
{
path: 'progetti/:id',
loadComponent: () => import('./project-page').then(m => m.ProjectPage),
resources: ({ params }) => ({
project: httpResource<Project>(() =>
`/api/projects/${encodeURIComponent(params()['id'])}`
),
}),
},
];
Qui succedono due cose interessanti. Il componente viene caricato quando serve, grazie a loadComponent. Nel frattempo, httpResource legge l’ID dai parametri reattivi e prepara la richiesta per quel progetto.
La risorsa si chiama project: tieni a mente questo nome, perché lo ritroveremo tra poco nel componente. In questa configurazione il Router aspetta i dati prima di attivare la pagina e passa all’input il valore caricato. Se la richiesta fallisce, anche la navigazione può fallire: per un’applicazione completa serve quindi una gestione degli errori di navigazione.
Volendo aprire subito la pagina e mostrare uno stato di caricamento al suo interno, esiste anche nonBlocking(). In quel caso cambia quello che ricevi: una risorsa da interrogare, non direttamente l’oggetto progetto. Per ora manteniamo l’esempio sul caricamento prima dell’apertura.
Il componente riceve i dati e li usa
A questo punto possiamo scrivere la nostra pagina. Niente copia del parametro in una variabile locale: dichiariamo quello che ci serve e usiamo i signals per derivare il resto.
project-page.ts
import { Component, computed, input } from '@angular/core';
import { RouterLink } from '@angular/router';
import type { Project } from './project';
@Component({
selector: 'app-project-page',
imports: [RouterLink],
template: `
<h1>{{ project().name }}</h1>
<p>Progetto: {{ id() }} · Completato al {{ progress() }}%</p>
<nav aria-label="Sezioni del progetto">
<a [routerLink]="['/progetti', id()]">Panoramica</a>
<a [routerLink]="['/progetti', id()]"
[queryParams]="{ tab: 'attivita' }">Attività</a>
</nav>
@if (activeTab() === 'attivita') {
<p>Attività completate: {{ project().completedTasks }}</p>
} @else {
<p>Qui trovi la panoramica del progetto.</p>
}
`,
})
export class ProjectPage {
readonly id = input.required<string>();
readonly tab = input<string>();
readonly project = input.required<Project>();
readonly activeTab = computed(() =>
this.tab() === 'attivita' ? 'attivita' : 'panoramica'
);
readonly progress = computed(() => {
const { completedTasks, totalTasks } = this.project();
return totalTasks === 0
? 0
: Math.round((completedTasks / totalTasks) * 100);
});
}
Ora id() contiene l’identificativo e project() i dati caricati dalla rotta. Con progress calcoliamo la percentuale senza mantenere un secondo valore da aggiornare a mano. È lo stesso ragionamento delle computed signal di cui abbiamo già parlato: se un valore dipende da altri valori, possiamo descrivere quella relazione direttamente.
Hai notato activeTab? Il query parameter è facoltativo, quindi tab() può essere undefined. Se manca, oppure contiene un valore che non riconosciamo, mostriamo la panoramica. Il tipo TypeScript da solo non controlla quello che qualcuno scrive nell’URL.
Anche input.required() non verifica che il progetto esista o che l’utente possa accedervi. Esprime ciò che il componente si aspetta di ricevere. I controlli sui dati e sui permessi rimangono responsabilità del backend.
Cambiare scheda senza duplicare lo stato
I due link aggiornano l’URL. Da lì il Router aggiorna l’input tab e la computed sceglie cosa mostrare. Non dobbiamo aggiungere un effect per copiare il query parameter in un altro signal.
Questo torna utile anche quando togli il parametro: tornando da ?tab=attivita al solo percorso del progetto, il binding imposta l’input senza corrispondenza a undefined. La pagina torna alla panoramica, invece di tenersi la vecchia scheda. E l’indirizzo continua a raccontare cosa stai guardando, anche se lo copi o usi i pulsanti avanti e indietro del browser.
E mentre il progetto si carica?
Se il Router aspetta la risposta del server, è utile far capire che sta succedendo qualcosa. Possiamo farlo nel componente principale, usando currentNavigation(). Per sapere se ci troviamo nella sezione progetti, invece, abbiamo la funzione isActive(), che restituisce un signal booleano.
app.ts
import { Component, computed, inject } from '@angular/core';
import { isActive, Router, RouterLink, RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterLink, RouterOutlet],
template: `
<a routerLink="/progetti/demo"
[class.active]="inProjects()">Progetti</a>
@if (navigating()) {
<p role="status">Sto aprendo il progetto...</p>
}
<router-outlet />
`,
})
export class App {
private readonly router = inject(Router);
readonly inProjects = isActive('/progetti', this.router);
readonly navigating = computed(() =>
this.router.currentNavigation() !== null
);
}
La computed navigating rimane vera durante la navigazione e torna falsa quando il Router è di nuovo inattivo, anche in caso di annullamento o errore. Non rappresenta però tutte le richieste HTTP dell’applicazione: per esempio, una risorsa non bloccante può continuare a caricare dopo l’apertura della pagina.
isActive considera per impostazione predefinita anche i percorsi figli: per questo /progetti risulta attivo mentre visiti /progetti/demo. Se vuoi soltanto applicare una classe a un link, anche RouterLinkActive resta una scelta comoda.
Quindi possiamo dimenticarci di RxJS?
Non proprio. In questa pagina non abbiamo avuto bisogno di sottoscrizioni manuali per leggere parametri e dati, ed è già un bel vantaggio. Ma ActivatedRoute, i resolver e Router.events continuano ad avere il loro posto. Se ti serve elaborare una sequenza di eventi con gli operatori RxJS, non c’è motivo di riscriverla solo per togliere un Observable.
Per una pagina come questa, invece, mi piace la separazione che otteniamo: la rotta descrive da dove arrivano i dati, il componente dichiara cosa riceve e le computed preparano ciò che mostriamo. Ogni pezzo ha un compito facile da riconoscere.
Conclusione
Usare il Router con i signals non significa cambiare tutto quello che sai sul routing di Angular. Significa sfruttare un collegamento che, una volta abilitato, ci evita parecchio lavoro ripetitivo.
La prossima volta che devi costruire una pagina di dettaglio, prova a partire dagli input: quali valori arrivano dall’URL? Quali dati servono prima di aprire la pagina? E cosa puoi derivare con una computed? Potresti scoprire che il tuo componente ha bisogno di molto meno codice di quanto pensassi!
Per approfondire
Lo spunto per questo articolo arriva dalla guida di Angular University sul Router con Signals. Per i dettagli delle API puoi consultare la documentazione Angular su binding degli input, Router Resources, httpResource, stato della navigazione e isActive.