
GUIDE PER ASPIRANTI PROGRAMMATORI
Routing in Angular
Se volessimo sintetizzare al massimo il funzionamento di un sito web classico, potremmo ridurre tutto a un continuo ping pong tra client e server, dove il primo fa le richieste e il secondo risponde con delle pagine HTML; queste pagine HTML, a loro volta, ci permettono di fare nuove richieste, generalmente in GET se vogliamo…


Vuoi avviare una nuova carriera o fare un upgrade?
Trova il corso Digital & Tech piĂą adatto a te nel nostro catalogo!
- Le direttive in Angular
- I componenti in Angular
- Il template in Angular
- Le direttive strutturali in Angular
- La content projection in Angular
- I servizi in Angular
- Le Pipes in Angular
- Routing in Angular
- Invio di form in Angular
- Built-in control flow in Angular
- Deferrable views in Angular
- Image optimization in Angular
Se volessimo sintetizzare al massimo il funzionamento di un sito web classico, potremmo ridurre tutto a un continuo ping pong tra client e server, dove il primo fa le richieste e il secondo risponde con delle pagine HTML; queste pagine HTML, a loro volta, ci permettono di fare nuove richieste, generalmente in GET se vogliamo navigare su un’altra pagina, oppure in POST se vogliamo inviare dei dati.
Questo meccanismo costituisce una ragione importante per cui normalmente l’esperienza di un’applicazione nativa è preferita dagli utenti alla navigazione sul web, specialmente per tutte quelle attività che ci permettono di utilizzare un qualche servizio in modo particolarmente interattivo.
Come altri framework frontend, Angular mira a colmare questo divario realizzando le cosiddette applicazioni a pagina singola (single page application, o SPA); questa definizione vale per tutti quei siti web che implementano una navigazione a frontend e quindi non richiedono mai il ricaricamento della pagina mentre vengono utilizzati. L’insieme di funzionalità che consentono la navigazione a frontend prende il nome di routing; il routing si occupa di:
- associare ad ogni percorso dell’URL una vista o pagina, rappresentata da un componente;
- supportare l’uso di parametri per fornire dati dinamici alla rotta attiva;
- valutare l’indirizzo di atterraggio al primo caricamento della web app e attivare la vista associata;
- implementare la navigazione a frontend sincronizzando la sostituzione della vista con il nuovo indirizzo e manipolando la cronologia del browser, simulando una navigazione reale.
In Angular tutte queste operazioni girano intorno alla service class Router, e la sua configurazione è generata automaticamente da ng new rispondendo y alla domanda: Would you like to add Angular routing?
In ogni applicazione Angular dotata di navigazione esiste un’unica istanza (singleton) della classe Router; il router espone metodi che consentono la navigazione ed eventi che possono essere intercettati per reagire alla navigazione. Insieme ad altri servizi, componenti, direttive, ecc, la classe Router è impacchettata nel RouterModule.
Il RouterModule è un NgModel, cioè un pacchetto di classi Angular (componenti, direttive, servizi, ecc) che possono essere importate insieme usando la proprietà imports passata al decoratore @Component.
Anche il CommonModule importato in precedenza è un NgModule! In quel caso, ci ha permesso di importare le direttive strutturali tutte insieme.
Il router module è configurato in app.config.ts tramite la funzione provideRouter che accetta come argomento la configurazione delle rotte di navigazione. Questa configurazione si trova in app.routes.ts e ha lo scopo di associare i percorsi di navigazione dell’app ai rispettivi componenti.
Siamo pronti per vedere un semplice esempio. Costruiamo un’app che contiene una generica pagina elenco e una pagina dettaglio; dagli elementi dell’elenco si navigherà verso i dettagli passando alla rotta un parametro identificativo. Aggiungeremo anche una pagina not found, nel caso in cui si atterri su un indirizzo non valido.
Creiamo le nostre pagine con la CLI:
ng g c pages/list ng g c pages/detail ng g c pages/not-found
Il comando g c è abbreviazione di generate component; con ng –help possiamo consultare tutti gli altri alias.
Ora associamo le pagine alle diverse rotte; la rotta dell’elenco è una semplice rotta statica, mentre quella del dettaglio include un parametro per identificare l’articolo; la rotta not found dovrà invece intercettare tutte le rotte non configurate.
In app.routes.ts:
import { Routes } from '@angular/router'; import { ListComponent } from './pages/list/list.component'; import { DetailComponent } from './pages/detail/detail.component'; import { NotFoundComponent } from './pages/not-found/not-found.component'; export const routes: Routes = [{ path: 'list', component: ListComponent }, { path: 'detail/:id', component: DetailComponent }, { path: '', redirectTo: 'list' }, { path: '**', component: NotFoundComponent }];
In questa configurazione abbiamo:
- una rotta con path statico;
- una rotta con un path parametrizzato da :id;
- una rotta che intercetta il path / e naviga verso list, la nostra home;
- una rotta “pigliatutto” che intercetta qualunque percorso (**) e mostra il nostro not found.
Va notato che l’ordine delle rotte conta: se la rotta “pigliatutto” ** fosse all’inizio dell’array, intercetterebbe anche i percorsi che hanno un componente associato e, dunque, la nostra app mostrerebbe sempre e solo not found.
Per poter visualizzare le pagine, nel nostro app component dobbiamo proiettare la pagina attiva; lo facciamo con il router-outlet:
import { Component } from '@angular/core'; import { RouterModule } from '@angular/router'; @Component({ selector: 'app-root', standalone: true, imports: [RouterModule], // <- import router module template: '<router-outlet />' // <- active page placeholder }) export class AppComponent { }
Il tag router-outlet è un segnaposto che indica al router dove collocare la pagina attiva; è importante sottolineare che il contenuto della pagina non verrà renderizzato dentro ma sotto al router-outlet, che rimarrà un tag vuoto nel codice HTML generato da Angular.
Implementiamo dunque le nostre varie pagine.
In list.component.html mettiamo l’elenco dei link di navigazione al dettaglio, passando diversi :id
<ul> <li><a routerLink="/detail/1">Item 1</a></li> <li><a routerLink="/detail/2">Item 2</a></li> <li><a routerLink="/detail/3">Item 3</a></li> </ul>
Al posto dell’attributo href incontriamo la direttiva routerLink ; per farla funzionare, dobbiamo importare RouterModule in list.component.ts:
import { Component } from '@angular/core'; import { RouterModule } from '@angular/router'; @Component({ selector: 'app-list', standalone: true, imports: [RouterModule], templateUrl: './list.component.html', styleUrl: './list.component.css' }) export class ListComponent { }
La direttiva routerLink è molto importante perché implementa lo stesso comportamento che avremmo utilizzando l’attributo href, salvo il fatto che la navigazione avviene tramite il Router, che è una sua dipendenza interna. Se utilizzassimo href, causeremmo una ricarica della pagina (e dunque un riavvio dell’intera app) ad ogni navigazione!
Nel nostro DetailComponent vogliamo mostrare l’identificativo ricevuto attraverso il percorso di navigazione; per farlo dobbiamo introdurre una nuova classe injectable: ActivatedRoute. Questa classe può essere iniettata nel componente associato alla rotta attiva e ci permetterà di consultare i dati di navigazione (parametri, query string, eccetera).
Vediamolo in detail.component.ts:
import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-detail', standalone: true, imports: [], templateUrl: './detail.component.html', styleUrl: './detail.component.css' }) export class DetailComponent { constructor(private route: ActivatedRoute) {} get id() { return this.route.snapshot.paramMap.get('id') } }
DetailComponent qui richiede ActivatedRoute come dipendenza alla DI, dopodiché espone un getter che estrae id dai parametri di navigazione.
Dunque detail.component.html avrĂ questo aspetto:
<p>Element with id: <code>{{ id }}</code></p>
Prima di andare avanti, facciamo qualche osservazione.
Anzitutto, abbiamo iniettato ActivatedRoute tramite costruttore; sarebbe stato equivalente farlo usando la funzione inject. L’API di ActivatedRoute espone perlopiù eventi rappresentati da proprietà di tipo Observable; possiamo navigare, invece, i dati già estratti attraverso la proprietà snapshot di tipo ActivatedRouteSnapshot; se ci servono solo i dati da questa classe, possiamo direttamente iniettarla al posto di ActivatedRoute.
L’uso dell’API basata su eventi diventa particolarmente comodo se si vuole avere un’API uniforme tra la navigazione e il recupero asincrono di dati, associando alla ricezione del parametro id una chiamata HTTP che recuperi i relativi dati; più avanti vedremo meglio come.
Poiché sia lavorare per eventi, sia estrarre i dati da uno snapshot può risultare macchinoso, il team di Angular ha introdotto un’API più leggera per accedere ai dati di navigazione: riceverli attraverso delle normalissime input property, come se fossero passati tramite il template del componente padre.
Per attivare questa API dobbiamo fare una piccola aggiunta in app.config.ts:
import { ApplicationConfig } from '@angular/core'; import { provideRouter, withComponentInputBinding } from '@angular/router'; import { routes } from './app.routes'; import { provideClientHydration } from '@angular/platform-browser'; import { provideHttpClient } from '@angular/common/http'; export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes, withComponentInputBinding()), // <- provideClientHydration(), provideHttpClient() ] };
Con quest’impostazione detail.component.ts diventa:
import { Component, Input } from '@angular/core'; @Component({ selector: 'app-detail', standalone: true, imports: [], templateUrl: './detail.component.html', styleUrl: './detail.component.css' }) export class DetailComponent { @Input() id!: string; // just this! }
A questo punto, se volessimo reagire alla ricezione del parametro id per innescare una qualche chiamata, ci basterebbe scrivere esplicitamente il setter della proprietĂ :
import { Component, Input } from '@angular/core'; @Component({ selector: 'app-detail', standalone: true, imports: [], templateUrl: './detail.component.html', styleUrl: './detail.component.css' }) export class DetailComponent { #id!: string; get id() { return this.#id } @Input() set id(value: string) { this.#id = value; this.#asyncHttpCall(); } async #asyncHttpCall() { // await ... } }
Nonostante l’estrema comodità di questo approccio, dobbiamo sottolineare due rischi: il primo è che stiamo in realtà facendo un uso improprio delle input property, o per lo meno ne stiamo implicitamente estendendo l’area di competenza; il secondo è che ci stiamo affidando a una magia del framework per associare le proprietà ai parametri, e dunque abbiamo possibilità più limitate nel gestire casi limite ed errori.
Prima di abbandonare questo argomento, andiamo a vedere come usare Router nella component class per navigare programmaticamente oppure intercettare gli eventi di navigazione. Per farlo, implementiamo un pulsante “Torna alla home” nella vista di dettaglio, ma anziché usare routerLink su un tag a useremo il metodo navigateByUrl sul router, iniettato nel componente.
In detail.component.ts:
import { Component, Input, inject } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-detail', standalone: true, imports: [], templateUrl: './detail.component.html', styleUrl: './detail.component.css' }) export class DetailComponent { router = inject(Router); @Input() id!: string; }
In detail.component.html:
<p>Element with id: <code>{{ id }}</code></p> <button (click)="router.navigateByUrl('/list')">Home</button>
Niente di particolarmente sorprendente o particolarmente utile data la semplicità di questo esempio; l’uso del router per navigare programmaticamente diventa, però, d’obbligo nel momento in cui la navigazione dovesse avvenire a seguito di altre logiche, ad esempio un salvataggio.
Per intercettare gli eventi di navigazione ci spostiamo invece in AppComponent che essendo il contenitore di router-outlet rimane sempre attivo.
In app.component.ts:
import { Component, OnInit, inject } from '@angular/core'; import { Router, RouterModule } from '@angular/router'; @Component({ selector: 'app-root', standalone: true, imports: [RouterModule], template: '<router-outlet />' }) export class AppComponent implements OnInit { router = inject(Router); ngOnInit(): void { this.router.events.subscribe(console.log); } }
In questo esempio abbiamo implementato l’interfaccia OnInit per agganciarci agli eventi del router, esposti dalla proprietà events sotto forma di un Observable; in questo modo andremo a loggare tutto quello che viene fuori dal router quando navighiamo.
Se andiamo nel browser e apriamo la console, vedremo che questi eventi rappresentano tutto il ciclo di navigazione (da NavigationStart a NavigationEnd); in una casistica più verosimile, se volessimo, ad esempio, agganciare un comportamento all’avvenuta navigazione, saremmo interessati esclusivamente all’evento NavigationEnd che chiude il ciclo:
import { Component, OnInit, inject } from '@angular/core'; import { EventType, Router, RouterModule } from '@angular/router'; import { filter } from 'rxjs'; @Component({ selector: 'app-root', standalone: true, imports: [RouterModule], template: '<router-outlet />' }) export class AppComponent implements OnInit { router = inject(Router); ngOnInit(): void { this.router.events .pipe(filter(e => e.type === EventType.NavigationEnd)) .subscribe(console.log); } }
Qui abbiamo usato il metodo Observable.pipe insieme all’operatore filter per generare un flusso di eventi che emette solo NavigationEnd.
CONTENUTI GRATUITI IN EVIDENZA
Guide per aspiranti programmatori 👨🏻‍🚀
Vuoi muovere i primi passi nel Digital e Tech? Abbiamo preparato alcune guide per aiutarti a orientarti negli ambiti piĂą richiesti oggi.