Angular in 10 steps - 4 of 10

04 - Il Router: da componenti a pagine
Nel capitolo scorso ti avevo promesso che avremmo trasformato i nostri componenti in vere e proprie pagine navigabili. Eccoci: è il momento del Router.
Una delle cose più belle delle applicazioni moderne è che passi da una "pagina" all'altra senza mai ricaricare davvero il browser. Niente schermata bianca, niente attesa: l'URL cambia e il contenuto pure, all'istante. In Angular tutto questo passa dal router, ed è uno dei concetti che userai in ogni singolo progetto.
In questo capitolo vediamo cos'è e come si configura, il lazy loading, come passare parametri nelle rotte, i modi per navigare, le guardie e i resolver. C'è parecchia carne al fuoco, ma ogni pezzo è corto.
🚪 Cos'è il Router
Il router è il sistema che gestisce la navigazione tra le pagine di una Single Page Application. E "pagine", in Angular, significa componenti: non esiste un file HTML diverso per ogni URL, esiste un componente diverso che il router monta e smonta in base all'indirizzo.
In una SPA il browser carica l'applicazione una volta sola. Da lì in poi è il router a decidere quale componente mostrare per ogni URL, senza altri round trip per l'HTML.
Un esempio concreto:
/mostraHomeComponent/gamesmostraGamesComponent/games/42mostra il dettaglio del gioco con id 42
Dove finiscono le pagine: router-outlet
Prima domanda da farsi: se il router "monta" i componenti, dove li mette?
La risposta è <router-outlet>, un segnaposto che piazzi nel template del
componente radice:
<!-- app.component.html -->
<header>La mia app</header>
<router-outlet />Tutto ciò che sta fuori dall'outlet (header, footer, menu) resta fisso; dentro
l'outlet il router inserisce la pagina corrente. Ricordati la lezione del capitolo
scorso: RouterOutlet è una direttiva e va importata nel componente che la usa.
1import { Component } from '@angular/core';
2import { RouterOutlet } from '@angular/router';
3
4@Component({
5 selector: 'app-root',
6 imports: [RouterOutlet],
7 templateUrl: './app.component.html',
8 styleUrl: './app.component.css'
9})
10export class AppComponent {}⚙️ Configurare le rotte
La configurazione vive in due file. Il primo è app.routes.ts, l'elenco delle
rotte:
1import { Routes } from '@angular/router';
2import { HomeComponent } from './home/home.component';
3
4export const routes: Routes = [
5 { path: '', component: HomeComponent },
6 {
7 path: 'games',
8 loadComponent: () =>
9 import('./games/games.component').then((m) => m.GamesComponent),
10 },
11];Il secondo è app.config.ts, dove le rotte vengono registrate con
provideRouter():
1import { ApplicationConfig } from '@angular/core';
2import { provideRouter } from '@angular/router';
3import { routes } from './app.routes';
4
5export const appConfig: ApplicationConfig = {
6 providers: [provideRouter(routes)],
7};Se hai creato il progetto con
ng new, questi due file esistono già e sono già collegati tra loro: ti basta riempire l'arrayroutes.
💤 Lazy loading
Nell'esempio sopra hai visto due modi di dichiarare una rotta. Con component
il componente finisce nel bundle iniziale dell'app; con loadComponent viene
caricato solo quando l'utente visita quella rotta. Questo è il lazy loading,
e i vantaggi sono concreti:
- il primo caricamento dell'app è più leggero e veloce
- il codice resta diviso in blocchi indipendenti per funzionalità
- le pagine che l'utente non visita mai non vengono proprio scaricate
Quel .then((m) => m.GamesComponent) serve perché import() restituisce l'intero
modulo JavaScript, e al router serve il componente. Se il componente è l'export
di default del file puoi anche scrivere solo loadComponent: () => import('./games/games.component'), ma la forma esplicita si legge meglio.
La mia regola pratica: la home eager, quasi tutto il resto lazy.
🔁 Parametri nella rotta
Il dettaglio del gioco 42 sta su /games/42: il 42 è un parametro di rotta
e si dichiara con i due punti:
1{
2 path: 'games/:id',
3 loadComponent: () =>
4 import('./game-detail/game-detail.component').then(
5 (m) => m.GameDetailComponent
6 ),
7}E come lo leggi dal componente? Il modo moderno è sorprendentemente pulito, ma
richiede un'opzione in più nella configurazione: withComponentInputBinding().
1import { provideRouter, withComponentInputBinding } from '@angular/router';
2import { routes } from './app.routes';
3
4export const appConfig: ApplicationConfig = {
5 providers: [provideRouter(routes, withComponentInputBinding())],
6};Con questa opzione attiva, Angular prende i parametri della rotta e li passa al componente come normali input. Nel componente scrivi:
1import { Component, input } from '@angular/core';
2
3@Component({ /* ... */ })
4export class GameDetailComponent {
5 id = input.required<string>();
6}Il nome dell'input deve combaciare con quello del parametro: la rotta dichiara
:id, l'input si chiamaid. Funziona allo stesso modo anche con le query string, tipo/search?q=zelda: un input chiamatoqriceve il valore.
Esiste anche la strada classica, iniettare ActivatedRoute e leggere
paramMap. La incontrerai in qualunque codebase esistente, ma per il codice
nuovo gli input binding sono più semplici e più facili da testare.
🚦 Navigare tra le rotte
Due modi, a seconda che la navigazione parta dal template o dalla logica.
Dal template, con la direttiva routerLink (da importare nel componente,
come sempre):
1<!-- rotta semplice -->
2<a routerLink="/home">Vai alla home</a>
3
4<!-- rotta con parametri -->
5<a [routerLink]="['/games', gameId]">Dettaglio gioco</a>
6
7<!-- con query string -->
8<a [routerLink]="['/search']" [queryParams]="{ q: 'zelda' }">Cerca Zelda</a>Bonus utile per i menu: routerLinkActive aggiunge una classe CSS quando il
link corrisponde alla rotta corrente.
<a routerLink="/home" routerLinkActive="active-link">Home</a>Dal codice, con il servizio Router, quando la navigazione dipende da una
logica: dopo il salvataggio di un form, dopo il login, e così via.
1import { Component, inject } from '@angular/core';
2import { Router } from '@angular/router';
3
4@Component({ /* ... */ })
5export class LoginComponent {
6 private router = inject(Router);
7
8 onLoginSuccess(gameId: string) {
9 // per segmenti e parametri
10 this.router.navigate(['/games', gameId]);
11
12 // per un URL completo come stringa
13 this.router.navigateByUrl('/home');
14
15 // con query string
16 this.router.navigate(['/search'], { queryParams: { q: 'zelda' } });
17 }
18}In sintesi: routerLink per i link veri e propri, router.navigate() quando
c'è una decisione di mezzo.
🛡 Guardie: proteggere le rotte
Una guardia decide se una rotta può essere attivata. Il caso tipico è la pagina riservata agli utenti autenticati. Oggi una guardia è una semplice funzione:
1import { inject } from '@angular/core';
2import { CanActivateFn, Router } from '@angular/router';
3
4export const authGuard: CanActivateFn = () => {
5 const isLoggedIn = localStorage.getItem('token') !== null;
6
7 // true lascia passare, un UrlTree reindirizza
8 return isLoggedIn ? true : inject(Router).parseUrl('/login');
9};Restituire false e basta funzionerebbe, ma l'utente resterebbe su una pagina
vuota: restituire un UrlTree lo porta invece dritto al login. E la agganci
alla rotta con canActivate:
1{
2 path: 'profile',
3 loadComponent: () =>
4 import('./profile/profile.component').then((m) => m.ProfileComponent),
5 canActivate: [authGuard],
6}Una guardia protegge la navigazione, non i dati: gira nel browser, quindi non è un meccanismo di sicurezza. I controlli veri restano sul server; la guardia serve a dare un'esperienza sensata a chi non è autorizzato.
🧩 Resolver: dati pronti prima della pagina
A volte una pagina senza i suoi dati non ha senso: tanto vale caricarli prima di mostrarla. È il lavoro dei resolver, anche loro semplici funzioni:
1import { inject } from '@angular/core';
2import { ResolveFn } from '@angular/router';
3import { GameService } from './services/game.service';
4import { Game } from './models/game';
5
6export const gameResolver: ResolveFn<Game> = (route) => {
7 const id = route.paramMap.get('id')!;
8 return inject(GameService).getGameById(id);
9};Si registra sulla rotta con resolve:
1{
2 path: 'games/:id',
3 loadComponent: () =>
4 import('./game-detail/game-detail.component').then(
5 (m) => m.GameDetailComponent
6 ),
7 resolve: { game: gameResolver },
8}E qui torna utile il withComponentInputBinding() di prima: anche i dati
risolti arrivano al componente come input, con il nome della chiave usata in
resolve.
1import { Component, input } from '@angular/core';
2import { Game } from './models/game';
3
4@Component({ /* ... */ })
5export class GameDetailComponent {
6 game = input.required<Game>();
7}Il rovescio della medaglia: finché il resolver non ha finito, la navigazione resta in attesa. Per chiamate veloci è perfetto; per dati lenti valuta se non sia meglio mostrare la pagina con uno spinner.
La rotta di riserva
Ultima cosa, piccola ma da fare sempre: cosa succede se l'utente digita un URL
che non esiste? Senza istruzioni, niente di buono. La rotta jolly ** cattura
tutto ciò che non ha trovato corrispondenza:
{ path: '**', component: NotFoundComponent }Va messa per ultima nell'array: le rotte vengono valutate in ordine, e il jolly mangia tutto quello che incontra.
Dove siamo arrivati
Il router è il pezzo che trasforma una collezione di componenti in un'applicazione: pagine con un URL, caricate solo quando servono, protette dove serve e con i dati già pronti quando ha senso. Con quello che hai visto qui copri la stragrande maggioranza dei casi reali.
Nel prossimo capitolo arriva il tema che ti ho anticipato due volte: data binding e signals, ovvero come Angular tiene sincronizzati dati e interfaccia. È il capitolo dove il framework moderno mostra i muscoli.