BACK TO THE SERIESGuida galattica ad Angular · 銀河のガイド

Angular in 10 steps - 4 of 10

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:


  • / mostra HomeComponent
  • /games mostra GamesComponent
  • /games/42 mostra 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:

HTML · コード
<!-- 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.

TYPESCRIPT · コード
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:

TYPESCRIPT · コード
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():

TYPESCRIPT · コード
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'array routes.

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

TYPESCRIPT · コード
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().

TYPESCRIPT · コード
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:

TYPESCRIPT · コード
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 chiama id. Funziona allo stesso modo anche con le query string, tipo /search?q=zelda: un input chiamato q riceve 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):

HTML · コード
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.

HTML · コード
<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.

TYPESCRIPT · コード
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:

TYPESCRIPT · コード
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:

TYPESCRIPT · コード
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:

TYPESCRIPT · コード
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:

TYPESCRIPT · コード
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.

TYPESCRIPT · コード
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:

TYPESCRIPT · コード
{ 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.