back to top

Rounting in Angular 2+ con Angular Router

In quest’ultima lezione vedremo come utilizzare Angular Router per creare delle applicazioni con più view e permettere all’utente di navigare fra le diverse sezioni senza dover ricaricare le pagine attraverso il browser. In questo modo riusciremo a creare una SPA (Single Page Application) che carica all’avvio una pagina iniziale e consente di muoversi fra diverse view in maniera dinamica. Angular fornisce il modulo RouterModule che contiene tutti gli strumenti di cui avremo bisogno per configurare la nostra applicazione in modo tale da mostrare una diversa view a seconda dell’indirizzo digitato nella barra del browser.

Per comprendere il funzionamento di Angular Router realizzeremo una semplice applicazione. Procederemo con lo sviluppo in più fasi. (Alla fine della lezione trovate il link alla versione finale dell’applicazione sulla piattaforma StackBlitz)

Creiamo la nostra applicazione: premessa

Iniziamo quindi a realizzare la prima versione e immaginiamo di creare un’applicazione, che chiamiamo ‘MyBox’, la quale offre ai clienti la possibilità di affittare uno spazio nei magazzini dell’azienda per conservare delle scatole con degli oggetti.

Nella prima versione ci limiteremo ad avere una pagina iniziale e configureremo Angular Router per poter gestire altri due percorsi, ‘/depositi’ e ‘/tariffe’, in corrispondenza dei quali saranno mostrati due componenti distinti che creiamo con l’ausilio di Angular CLI. In più aggiungeremo PageNotFoundComponent che sarà visualizzato ogni volta che si visita un percorso diverso da quelli elencati sopra. In questo modo potremo indicare all’utente che si è verificato un errore di tipo 404 e la pagina non è stata trovata.

Come configurare Angular Router

Procediamo allora a creare una nuova applicazione all’interno di una cartella vuota e serviamoci di Angular CLI per completare tutte le operazioni ripetitive.

ng new my-angular-app --no-interactive --prefix simple

Sempre con Angular CLI generiamo i primi 5 componenti della nostra applicazione in modo da ottenere una struttura iniziale come quella riportata sotto.

tree src/app
src/app
├── app.component.css
├── app.component.html
├── app.component.ts
├── app.module.ts
├── home
│   ├── home.component.html
│   └── home.component.ts
├── locations-list
│   ├── locations-list.component.css
│   ├── locations-list.component.html
│   └── locations-list.component.ts
├── navbar
│   ├── navbar.component.html
│   └── navbar.component.ts
├── page-not-found
│   ├── page-not-found.component.html
│   └── page-not-found.component.ts
└── plans
    ├── plans.component.css
    ├── plans.component.html
    └── plans.component.ts

5 directories, 16 files

Angular Router, LocationStrategy e l’elemento <base>

Prima di continuare, apriamo però un momento il file index.html presente nella cartella src.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>MyBox - Self Storage</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <simple-root></simple-root>
</body>
</html>

Come possiamo notare dal frammento di codice riportato sopra, immediatamente dopo l’elemento <title> è presente un elemento <base href="/"> avente un attributo href impostato al valore ‘/’.

Con questo accorgimento indichiamo l’URL di base rispetto al quale sono relativi tutti i percorsi dell’applicazione e si tratta di un elemento necessario per poter utilizzare lo stile HTML5 degli URL.

Infatti in Angular 2+ la strategia di default per la composizione degli indirizzi impiegata da Angular Router prende il nome di PathLocationStrategy e si avvale della History API introdotta in HTML5. In particolare utilizzando history.pushState() viene aggiornato l’url senza che il browser effettui una nuova richiesta al server e senza la necessità di usare un frammento hash come avveniva in Angular 1.x (È comunque possibile utilizzare la strategia HashLocationStrategy che impiega un frammento di hash – per esempio http://localhost:4200/#/tariffe – abilitandola esplicitamente in fase di configurazione delle diverse ‘route’).

Con la strategia di default avremo invece dei ‘normali’ indirizzi (http://localhost:4200/tariffe) a partire dalla base indicata nel file index.html (/). Se avessimo indicato come valore dell’attributo href dell’elemento <base> qualcosa diverso, per esempio <base href="/my-app">, la pagina iniziale della nostra applicazione sarebbe stata servita all’indirizzo http://localhost:4200/my-app/ e tutti gli altri percorsi sarebbero stati automaticamente costruiti a partire dalla nuova base, per esempio http://localhost:4200/my-app/tariffe.

Aggiungere Bootstrap all’applicazione

Apriamo una breve parentesi per dire che per questo progetto abbiamo usato ancora una volta Bootstrap che abbiamo installato digitando nella cartella base dell’applicazione il comando npm install bootstrap --save. Fatto ciò abbiamo aggiornato il file angular.json aggiungendo un elemento all’array styles relativo all’applicazione ‘my-angular-app’.

"styles": [
  "node_modules/bootstrap/dist/css/bootstrap.min.css",
  "src/styles.css"
]

Passi da seguire per la configurazione iniziale di Angular Router

A questo punto siamo pronti per effettuare le modifiche necessarie al progetto che consentiranno di completare la versione iniziale della nostra applicazione.

Prima di tutto dobbiamo definire i diversi percorsi dell’applicazione e per ora lo facciamo all’interno del file app.module.ts in cui creiamo un array di tipo Routes, ovvero un array di oggetti di tipo Route.

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'tariffe', component: PlansComponent },
  { path: 'prezzi', redirectTo: '/tariffe'},
  { path: 'depositi', component: LocationsListComponent },
  { path: '**', component: PageNotFoundComponent}
];

Ciascun oggetto ha una proprietà path pari al percorso in corrispondenza del quale vogliamo venga mostrato un certo componente che indichiamo con la proprietà component. Abbiamo poi usato due oggetti leggermente diversi dagli altri. Notate infatti che nell’ultimo oggetto dell’array abbiamo assegnato il simbolo ‘**’ alla proprietà path. Si tratta della cosiddetta wildcard route, una sorta di percorso jolly che intercetta qualsiasi URL.

È quindi bene sottolineare una proprietà importante da tenere in mente quando si definisce l’oggetto Routes. Infatti l’ordine in cui vengono inseriti i diversi oggetti di tipo Route nell’array è rilevante in quanto Angular Router, dovendo decidere quale componente mostrare in corrispondenza di un certo percorso, inizia a scorrere l’array dal primo oggetto e si ferma non appena ne trova uno con una proprietà ‘path’ che corrispone al percorso. Per questo motivo è necessario inserire l’oggetto avente la wildcard route come proprietà path alla fine dell’array altrimenti verrebbero individuati tutti i percorsi e verrebbe sempre mostrato il componente PageNotFoundComponent.

Abbiamo poi usato un altro oggetto con una proprietà redirectTo che fa in modo di redirigere il browser all’indirizzo con percorso pari a ‘/tariffe’ ogni volta che si accede al percorso ‘/prezzi’. L’oggetto in questione presenta anche una terza proprietà opzionale ‘pathMatch’ di tipo stringa che abbiamo omesso in quanto vogliamo avvalerci del valore di default pari a ‘prefix’. In alternativa è possibile usare il valore ‘full’. In quest’ultimo caso si ha una corrispondenza solo se il percorso è esattamente ‘/prezzi’, usando invece ‘prefix’ l’utente viene rediretto alla pagina delle tariffe anche se nella barra degli indirizzi digita qualcosa come ‘http://localhost:4200/prezzi/2019/aggiornamento’.

Il metodo statico forRoot()

Dopo aver completato la configurazione delle varie Route, dovremo registrarle con il modulo base AppModule. Per far ciò elencheremo nell’array imports di AppModule l’oggetto restituito dal metodo statico RouterModule.forRoot(routes). Il metodo statico forRoot() rappresenta una convenzione usata in Angular che possiamo utilizzare anche nei moduli da noi definiti e restituisce un oggetto che rispecchia la struttura dell’interfaccia ModuleWithProviders la quale, come definito nella documentazione, è un un wrapper attorno ad un NgModule che associa quest’ultimo a dei provider. In questo modo risulta chiaro che l’array imports dell’oggetto passato al decoratore @NgModule() accetta sia altri NgModule che oggetti di tipo ModuleWithProviders.

interface ModuleWithProviders<T> {
  ngModule: Type<T>
  providers?: Provider[]
}
routerModule with providers

Il motivo per cui viene utilizzato il metodo forRoot() è riconducibile all’argomento affrontato nella precedente lezione relativo al rapporto fra i diversi tipi di moduli (Eager e Lazy), la gerarchia di Injector e la visibilità dei servizi all’interno dell’applicazione.

In base a quanto detto, se abbiamo un modulo condiviso (ovvero importato in altri moduli) ed elenchiamo un certo servizio nell’array dei providers di tale modulo, verrà creata un’istanza del servizio registrata attraverso un provider con il Root Injector e comune a tutti i moduli caricati all’avvio dell’applicazione (Eagerly loaded). Se però introduciamo un modulo che viene caricato a richiesta quando è necessario (Lazy Loaded) e se quest’ultimo importa il suddetto modulo, verrà creata una nuova istanza del servizio visibile solo all’interno del modulo caricato a richiesta.

Per risolvere questo problema ed avere quindi una sola istanza di uno o più servizi in tutta l’applicazione, una soluzione possibile prevede la definizione di un metodo statico forRoot(). Supponiamo quindi di avere un modulo condiviso SharedModule. In quest’ultimo definiamo un metodo statico forRoot() che restituisce un oggetto di tipo ModuleWithProviders avente un array di providers per i vari servizi che vogliamo condividere. Invocheremo il metodo SharedModule.forRoot() solo all’interno del modulo base. Negli altri moduli, ci limiteremo eventualmente ad importare il modulo SharedModule per aver accesso ad eventuali componenti, pipe o direttive esportate.

import { NgModule, ModuleWithProviders } from '@angular/core';

import { LoggerService } from './logger.service';

@NgModule({
  declarations: [
    // ...
  ],
  imports: [
    // ...
  ],
  exports: [
    // ...
  ]
})
export class SharedModule {
  static forRoot(): ModuleWithProviders {
    return {
      ngModule: SharedModule,
      providers: [ LoggerService ]
    }
  }
}

Come detto invochiamo il metodo SharedModule.forRoot() in AppModule.

import { NgModule } from '@angular/core';
import { BrowserModule  } from '@angular/platform-browser';

import { SharedModule } from './shared/shared.module';
import { AppComponent } from './app.component';
import { Component1 } from './component1.component';
import { Component2 } from './component2.component';

@NgModule({
  imports: [
    BrowserModule,
    SharedModule.forRoot() // <-- solo nel modulo base
  ],
  declarations: [
    AppComponent,
    Component1,
    Component2
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Se abbiamo bisogno delle funzionalità esportate da SharedModule, ci limitiamo ad importare il modulo negli altri.

import { NgModule } from '@angular/core';

import { SharedModule } from '../shared/shared.module';

import { LazyComponent }   from './lazy.component';

@NgModule({
  imports: [
    SharedModule
  ],
  declarations: [LazyComponent]
})
export class LazyModule {}

Tornando a parlare di Angular Router, per le ragioni appena esposte invochiamo il metodo RouterModule.forRoot() all’interno del modulo base in modo da registrare i percorsi, configurare e inizializzare il router stesso. Quando creeremo dei Feature Module, in questi ultimi useremo invece il metodo RouterModule.forChild() che si limita solamente a registrare con Angular Router i percorsi definiti nei moduli secondari.

Infatti se il RouterModule non disponesse del metodo di forRoot(), verrebbero create più istanze del router, il che porterebbe al mal funzionamento dell’applicazione dato che non esisterebbe un solo router comune. Il metodo forRoot() restituisce quindi un oggetto con tutti i provider che permettono di registrare una sola istanza dei diversi servizi con il Root Injector.

Ritornando a parlare del nostro esempio, dopo aver apportato le oppourtune modifiche al file app.module.ts, nella prima versione della nostra applicazione avremo un modulo base definito dal codice riportato sotto.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, ModuleWithProviders } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { AppComponent } from './app.component';
import { NavbarComponent } from './navbar/navbar.component';
import { HomeComponent } from './home/home.component';
import { LocationsListComponent } from './locations-list/locations-list.component';
import { PlansComponent } from './plans/plans.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'tariffe', component: PlansComponent },
  { path: 'prezzi', redirectTo: '/tariffe'},
  { path: 'depositi', component: LocationsListComponent },
  { path: '**', component: PageNotFoundComponent}
];

const routerModuleWithProviders: ModuleWithProviders<RouterModule> =
  RouterModule.forRoot(
    routes,
    { enableTracing: true } // <-- debugging purposes only
  );

@NgModule({
  declarations: [
    AppComponent,
    NavbarComponent,
    HomeComponent,
    LocationsListComponent,
    PlansComponent,
    PageNotFoundComponent
  ],
  imports: [
    BrowserModule,
    routerModuleWithProviders
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Notate che al metodo RouterModule.forRoot() abbiamo passato come secondo argomento un oggetto il quale fa in modo che vengano stampate nella console degli sviluppatori del browser le informazioni relative all’attività del Router.

La direttiva RouterOutlet

Abbiamo visto come configurare Angular Router per registrare i diversi percorsi e attivare i rispettivi componenti. A questo punto servirà una sorta di slot in cui Angular Router dovrà di volta in volta caricare il corretto componente per mostrarlo all’interno della pagina. Per questo motivo Angular Router esporta una direttiva, detta RouterOutlet, che andremo ad usare nel template di AppComponent come se fosse un elemento HTML. Questo agirà da segnaposto indicando il punto in cui dovranno essere inseriti i vari componenti in corrispondenza dell’attivazione di un dato percorso.

<router-outlet></router-outlet>
<!-- I componenti relativi ai vari percosi vengono inseriti -->
<!-- come elementi adiacenti a <router-outlet> -->
<!-- quando il percorso viene attivato -->

Riportiamo quindi il codice presente nel file app.component.html.

<simple-navbar></simple-navbar>
<div class="container-fluid">
  <div class="row">
    <div class="col-md-12">
      <router-outlet></router-outlet>
    </div>
  </div>
</div>

Nel frammento di codice riportato sopra, utilizziamo vari elementi <div> con delle classi specifiche di Bootstrap che consentono di dare una certa struttura alla nostra applicazione. Notate che abbiamo inserito la direttiva <router-outlet> che sarà usata come segnaposto per l’inserimento dei vari componenti da parte di Angular Router. Abbiamo anche creato un componente NavbarComponent che mostrerà sempre una barra di navigazione in alto, indipendentemente dal percorso visitato.

Di seguito trovate invece il contenuto dei due file navbar.component.html e navbar.component.ts in cui abbiamo inserito il codice per creare una barra di navigazione che in dispositivi con viewport di larghezza inferiore a 768px mostra un pulsante per visualizzare gli elementi del menù. Anche in questo caso utilizziamo le classi CSS messe a disposizione da Bootstrap e all’interno della classe TypeScript NavbarComponent usiamo una proprietà booleana per capire se gli elementi del menù di navigazione devono essere visibili o meno.

import { Component } from '@angular/core';

@Component({
  selector: 'simple-navbar',
  templateUrl: './navbar.component.html',
  styles: []
})
export class NavbarComponent {

  activeNavbar = false;

  toggleNavbar() {
    this.activeNavbar = !this.activeNavbar;
  }

}
<!-- navbar.component.html -->
<nav class="navbar navbar-expand-md navbar-light bg-faded">
  <a class="navbar-brand" routerLink="/">
    <h1><img src="assets/my-box-logo.svg"  alt="My Box"></h1>
  </a>

  <button class="navbar-toggler" type="button" (click)="toggleNavbar()">
    <span class="navbar-toggler-icon"></span>
  </button>


  <div class="collapse navbar-collapse" [class.show]="activeNavbar">
    <ul class="navbar-nav ml-auto text-uppercase">
      <li class="nav-item">
        <a 
          class="nav-link" 
          routerLink="/depositi" 
          routerLinkActive="active">Depositi</a>
      </li>

      <li class="nav-item">
        <a 
          class="nav-link" 
          routerLink="/tariffe" 
          routerLinkActive="active">Tariffe</a>
      </li>
    </ul>
  </div>
</nav>

Dopo aver modificato il contenuto del file home.component.html, otteniamo il risultato mostrato nel video riportato sotto.

<!-- home.component.html -->
<div class="d-flex flex-wrap justify-content-around">
  <div>
    <h2 class="display-4">Hai bisogno di spazio?</h2>
    <p class="lead">MyBox &#232; la soluzione che fa per te.</p>
    <a routerLink="/tariffe" class="btn btn-outline-primary mr-2">Tariffe</a>
    <a routerLink="/depositi" class="btn btn-outline-secondary mx-2">I nostri centri</a>
  </div>
  <div>
    <img src="assets/my-box.svg" alt="Scatola con logo mybox">
  </div>
</div>

Come potete notare il passaggio da una view all’altra avviene in maniera dinamica senza che venga ricaricata ogni volta la pagina del browser. (A parte ovviamente quando si digita un nuovo percorso nella barra degli indirizzi.)

Refactoring della configurazione di routing in un modulo di routing

A questo punto abbiamo una versione funzionante della nostra applicazione anche se non abbiamo ancora implementato correttamente i componenti da visualizzare per i percorsi ‘/tariffe’ e ‘/depositi’. Se però torniamo al frammento di codice presente nel file app.module.ts, notiamo che possiamo isolare la configurazione di Angular Router e inserirla in un file separato in modo da strutturare ancora meglio la nostra applicazione.

Procediamo allora alla realizzazione di un nuovo modulo AppRouting all’interno del quale inseriamo solo la configurazione del Router.

ng generate module app-routing --module app --flat

Con il comando riportato sopra generiamo un nuovo modulo all’interno del file app-routing.module.ts in cui isoliamo le funzionalità e la configurazione di Angular Router.

// app-routing.module.ts
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';

import { HomeComponent } from './home/home.component';
import { LocationsListComponent } from './locations-list/locations-list.component';
import { PlansComponent } from './plans/plans.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'tariffe', component: PlansComponent },
  { path: 'prezzi', redirectTo: '/tariffe'},
  { path: 'depositi', component: LocationsListComponent },
  { path: '**', component: PageNotFoundComponent}
];

const routerModuleWithProviders: ModuleWithProviders<RouterModule> =
  RouterModule.forRoot(
    routes,
    { enableTracing: true } // <-- debugging purposes only
  );

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    routerModuleWithProviders
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule { }

Importiamo quindi il nuovo modulo AppRoutingModule all’interno del modulo base AppModule. Notate che abbiamo elencato RouterModule nell’array exports dell’oggetto passato al decoratore @NgModule() applicato al modulo AppRoutingModule. Esportando nuovamente RouterModule, i componenti dichiarati in AppModule (modulo in cui importiamo AppRoutingModule) avranno accesso alle direttive del router come routerlink e router-outlet. Diversamente visualizzeremo un messaggio di errore nella console degli strumenti per sviluppatori.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { AppComponent } from './app.component';
import { NavbarComponent } from './navbar/navbar.component';
import { HomeComponent } from './home/home.component';
import { LocationsListComponent } from './locations-list/locations-list.component';
import { PlansComponent } from './plans/plans.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  declarations: [
    AppComponent,
    NavbarComponent,
    HomeComponent,
    LocationsListComponent,
    PlansComponent,
    PageNotFoundComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Creare un ‘feature module’ per le tariffe

Continuiamo a sviluppare la nostra applicazione e creiamo un nuovo Feature Module per la sezione della nostra applicazione relativa alle tariffe. Procediamo quindi a realizzare il modulo PlansModule all’interno della cartella plans in modo da ottenere alla fine la seguente struttura dell’applicazione.

tree src/app
src/app
├── app-routing.module.ts
├── app.component.css
├── app.component.html
├── app.component.ts
├── app.module.ts
├── home
│   ├── home.component.html
│   └── home.component.ts
├── locations-list
│   ├── locations-list.component.css
│   ├── locations-list.component.html
│   └── locations-list.component.ts
├── navbar
│   ├── navbar.component.html
│   └── navbar.component.ts
├── page-not-found
│   ├── page-not-found.component.html
│   └── page-not-found.component.ts
└── plans
    ├── plan-detail
    │   ├── plan-detail.component.css
    │   ├── plan-detail.component.html
    │   └── plan-detail.component.ts
    ├── plan-list
    │   ├── plan-list.component.css
    │   ├── plan-list.component.html
    │   └── plan-list.component.ts
    ├── plan.model.ts
    ├── plan.service.ts
    ├── plans-mock.ts
    ├── plans-routing.module.ts
    └── plans.module.ts

7 directories, 25 files

Notate che nella nuova versione dell’applicazione abbiamo rimosso PlansComponent e all’interno della cartella plans abbiamo invece due nuovi componenti PlanListComponent e PlanDetailComponent. Abbiamo inoltre creato i file relativi al nuovo modulo, ovvero plans-routing.module.ts e plans.module.ts e abbiamo anche aggiunto il servizio PlanService che restituisce degli oggetti i quali implementano l’interfaccia Plan definita nel file plan.model.ts. Per semplicità PlanService lavorerà su dei dati fittizi presenti nel file plans-mock.ts.

Per realizzare quindi la nuova sezione della nostra appplicazione iniziamo a creare il nuovo modulo servendoci ancora una volta di Angular CLI.

ng generate module plans/plans --module app --flat --routing
CREATE src/app/plans/plans-routing.module.ts (249 bytes)
CREATE src/app/plans/plans.module.ts (279 bytes)
UPDATE src/app/app.module.ts

Il comando riportato sopra provvederà a creare i file relativi a PlansModule compreso un modulo separato per la configurazione dei percorsi relativi alle tariffe. Viene quindi aggiornato il modulo base in cui viene importato lo stesso PlansModule.

Il passo successivo consiste nell’aggiornare i file app-rountig.module.ts e app.module.ts. Nel primo dovremo infatti rimuovere le informazioni relative al percorso ‘/tariffe’ che inseriremo invece in plans/plans-routing.module.ts

// app-routing.module.ts
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';

import { HomeComponent } from './home/home.component';
import { LocationsListComponent } from './locations-list/locations-list.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'depositi', component: LocationsListComponent },
  { path: '**', component: PageNotFoundComponent}
];

const routerModuleWithProviders: ModuleWithProviders<RouterModule> =
  RouterModule.forRoot(
    routes,
    { enableTracing: true } // <-- debugging purposes only
  );

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    routerModuleWithProviders
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule { }

Nel file app.module.ts dovremo innanzitutto rimuovere PlansComponent dall’array declarations e assicurarci che PlansModule sia elencato prima di AppRoutingModule all’interno dell’array imports. Ciò è necessario perché, nel cercare quale componente deve essere mostrato per un dato percorso, Angular Router procede in ordine scorrendo gli elementi degli array di tipo Routes. Siccome andremo a definire un array di questo tipo per i percorsi relativi alle tariffe nel file plans-routing.module.ts, se PlansModule non fosse inserito prima di AppRoutingModule, nessuno dei percorsi relativi alle tariffe potrebbe essere attivato visto che nell’array routes, presente in app-routing.module.ts, abbiamo inserito un oggetto relativo ad una Wildcard route che, precedendo i percorsi relativi alle tariffe, farebbe in modo che per questi ultimi verrebbe sempre mostrato il componente PageNotFoundComponent.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { AppComponent } from './app.component';
import { NavbarComponent } from './navbar/navbar.component';
import { HomeComponent } from './home/home.component';
import { LocationsListComponent } from './locations-list/locations-list.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { AppRoutingModule } from './app-routing.module';
import { PlansModule } from './plans/plans.module';

@NgModule({
  declarations: [
    AppComponent,
    NavbarComponent,
    HomeComponent,
    LocationsListComponent,
    PageNotFoundComponent
  ],
  imports: [
    BrowserModule,
    // È indispensabile che PlansModule
    // preceda AppRoutingModule
    PlansModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

A questo punto possiamo creare i file necessari per il nuovo modulo. Iniziamo quindi a generare i due componenti all’interno della cartella plans.

ng generate component plans/plan-list --no-spec
CREATE src/app/plans/plan-list/plan-list.component.css (0 bytes)
CREATE src/app/plans/plan-list/plan-list.component.html (31 bytes)
CREATE src/app/plans/plan-list/plan-list.component.ts (295 bytes)
UPDATE src/app/plans/plans.module.ts (557 bytes)
ng generate component plans/plan-detail --no-spec
CREATE src/app/plans/plan-detail/plan-detail.component.css (0 bytes)
CREATE src/app/plans/plan-detail/plan-detail.component.html (31 bytes)
CREATE src/app/plans/plan-detail/plan-detail.component.ts (295 bytes)
UPDATE src/app/plans/plans.module.ts (557 bytes)

Creiamo poi il file plan.model.ts.

ng generate interface plans/plan --type=model
CREATE src/app/plans/plan.model.ts (27 bytes)
export interface Plan {
  name: string;
  price: number;
  details: string[];
  colorClass: string;
}

Aggiungiamo dunque dei dati fittizi all’interno del file plans/plans-mock.ts

import { Plan } from './plan.model';

export const PLANS: Plan[] = [
  {
    name: 'base',
    price: 5,
    details: [
      '2 Scatole',
      '10 Kg per scatola',
      'Consegna a domicilio in 72 ore',
      'Ritiro a domicilio in 48 ore'
    ],
    colorClass: 'primary'
  },
  {
    name: 'standard',
    price: 10,
    details: [
      '4 Scatole',
      '10 Kg per scatola',
      'Consegna a domicilio in 48 ore',
      'Ritiro a domicilio in 24 ore'
    ],
    colorClass: 'success'
  },
  {
    name: 'pro',
    price: 20,
    details: [
      '8 Scatole',
      '15 Kg per scatola',
      'Consegna a domicilio in 24 ore',
      'Ritiro a domicilio in 24 ore'
    ],
    colorClass: 'danger'
  }
];

E definiamo il servizio che consentirà ai componenti di accedere ai dati riportati sopra.

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';

import { Plan } from './plan.model';
import { PLANS } from './plans-mock';

@Injectable({
  providedIn: 'root'
})
export class PlanService {

  getPlans(): Observable<Plan[]> {
    return of(PLANS);
  }

  getPlan(planName: string) {
    return this.getPlans().pipe(
      map((plans: Plan[]) => plans.find(plan => plan.name === planName))
    );
  }
}

Prima di analizzare in dettaglio i due componenti PlanListComponent e PlanDetailComponent, vediamo il contenuto dei due file plans.module.ts e plans-routing.module.ts

// plans.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { PlansRoutingModule } from './plans-routing.module';
import { PlanListComponent } from './plan-list/plan-list.component';
import { PlanDetailComponent } from './plan-detail/plan-detail.component';

@NgModule({
  declarations: [PlanListComponent, PlanDetailComponent],
  imports: [
    CommonModule,
    PlansRoutingModule
  ]
})
export class PlansModule { }

PlansModule presenta nell’array declarations i due componenti PlanListComponent e PlanDetailComponent, importa il modulo PlansRoutingModule e CommonModule visto che si tratta di un modulo secondario.

// plans-routing.module.ts
import { NgModule, ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { PlanListComponent } from './plan-list/plan-list.component';
import { PlanDetailComponent } from './plan-detail/plan-detail.component';

const routes: Routes = [
  { path: 'tariffe',  component: PlanListComponent },
  { path: 'tariffe/:plan', component: PlanDetailComponent },
  { path: 'prezzi', redirectTo: '/tariffe'}
];

const planChildRoutes: ModuleWithProviders<RouterModule> = 
  RouterModule.forChild(routes);

@NgModule({
  imports: [planChildRoutes],
  exports: [RouterModule]
})
export class PlansRoutingModule { }

Il file plans-routing.module.ts contiene il codice del modulo PlansRoutingModule in cui definiamo l’array routes relativo ai percorsi che riguardano le tariffe. Al contrario di quanto avviene in AppModule, in questo caso invochiamo il metodo statico RouterModule.forChild(routes) che si limita a registrare i percorsi definiti nel modulo con la singola istanza del Router dell’applicazione.

Notiamo anche che il secondo oggetto dell’array routes contiene una proprietà path che differisce da quelle viste finora. In particolare abbiamo usato la sintassi 'tariffe/:plan', in cui abbiamo il carattere due punti seguito dal nome di un parametro (:nome-del-parametro). Nel nostro caso abbiamo scelto ‘plan’ come nome del nostro parametro. Vedremo a breve che potremo accedere al suo valore all’interno del componente PlanDetailComponent tramite le API fornite da Angular Router.

Grazie all’uso del parametro ‘plan’ chiediamo ad Angular Router di attivare il componente PlanDetailComponent ogni volta che si ha un percorso come ‘/tariffe/nome-della-tariffa’. Nel caso specifico all’interno del template di PlanListComponent avremo dei link come ‘/tariffe/base’. Quest’ultimo consente di visualizzare i dettagli della tariffa più economica del servizio offerto.

Riportiamo allora il codice del componente PlanListComponent a partire dal file plan-list.component.ts.

// plan-list.component.ts
import { Component, OnInit } from '@angular/core';

import { PlanService } from '../plan.service';
import { Plan } from '../plan.model';
import { Observable } from 'rxjs';

@Component({
  selector: 'simple-plan-list',
  templateUrl: './plan-list.component.html',
  styleUrls: ['./plan-list.component.css']
})
export class PlanListComponent implements OnInit {
  plan$: Observable<Plan[]>;

  constructor(private planService: PlanService) { }

  ngOnInit() {
    this.plan$ = this.planService.getPlans();
  }

}

All’interno di PlanListComponent iniettiamo un’istanza di PlanService e nel metodo ngOnInit() inizializziamo la proprietà plan$ a cui assegniamo un riferimento ad un oggetto di tipo Observable che emette come valore un array di oggetti di tipo Plan. In questo caso non invochiamo il metodo observable.subscribe() perché all’interno del template del componente usiamo la pipe async che si occuperà di farlo al posto nostro. In questo modo potremo inserire nel file plan-list.component.html il seguente frammento di codice.

<h2  class="text-center mb-5 display-2">Tariffe</h2>
<div class="row">
  <div class="col-lg-4 mb-5" *ngFor="let plan of plan$ | async" >
    <div class="card border-{{ plan.colorClass }}">
      <div class="card-header bg-{{ plan.colorClass }} text-white">
        <h3 class="text-center">Piano {{ plan.name | titlecase }}</h3>
      </div>
      <div class="card-body text-center">
        <p class="display-4">
          <strong>
            {{ plan.price | number:'1.0-2' }} &#8364;/mese
          </strong>
        </p>
      </div>
      <ul class="list-group list-group-flush text-center">
        <li 
          class="list-group-item" 
          *ngFor="let detail of plan.details">
            {{ detail }}
        </li>
      </ul>
      <div class="card-footer text-center">
        <a
          routerLink="{{ plan.name }}"
          class="btn btn-lg btn-{{ plan.colorClass }}">
          Dettagli
        </a>
      </div>
    </div>
  </div>
</div>

Utilizziamo la direttiva NgFor per generare il codice necessario per ciascuno dei tre piani contenuti nell’array emesso dall’Observable plan$.

pagina tariffe dell'esempio di un'applicazione con angular routing

Come possiamo vedere dall’immagine, ciascun piano tariffario presenta un link ‘Dettagli’ per visualizzare le informazioni specifiche di ognuno. Per esempio nel caso del piano base è un link all’indirizzo ‘http://localhost:4200/tariffe/base‘ in corrispondenza del quale viene attivato il componente PlanDetailComponent che avrà accesso al paramentro ‘plan’ con valore pari a ‘base’.

Vediamo allora il codice sorgente del componente PlanDetailComponent e del suo template.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';

import { PlanService } from './../plan.service';
import { Observable, Subscription } from 'rxjs';
import { Plan } from '../plan.model';

@Component({
  selector: 'simple-plan-detail',
  templateUrl: './plan-detail.component.html',
  styleUrls: ['./plan-detail.component.css']
})
export class PlanDetailComponent implements OnInit, OnDestroy {

  plan: Plan;
  planSubscription: Subscription;

  constructor(
    private planService: PlanService,
    private router: Router,
    private activatedRoute: ActivatedRoute
  ) { }

  ngOnInit() {
    const plan = this.activatedRoute.snapshot.paramMap.get('plan');

    this.planSubscription = this.planService
      .getPlan(plan)
      .subscribe(value => {
        if (value) {
          this.plan = value;
        } else {
          this.router.navigate(['/404']);
        }
      });
  }

  ngOnDestroy() {
    this.planSubscription.unsubscribe();
  }
}

Dal frammento di codice riportato sopra vediamo che in PlanDetailComponent iniettiamo tre servizi: il servizio da noi realizzato per la lista dei piani tariffari e due servizi forniti da Angular, Router e ActivatedRoute. Nel metodo ngOnInit() utilizziamo la proprietà snapshot del servizio ActivatedRoute per recuperare il valore del parametro ‘plan’ relativo al percorso corrente. È bene sottolineare che in questo modo otteniamo una e una sola volta il valore del parametro ‘plan’ durante la fase di inizializzazione del componente (Vedremo a breve quali sono le conseguenze di questa scelta). Usiamo quindi la stringa contenuta nel parametro ‘plan’ per prelevare le informazioni del piano tariffario corrispondente. Nel caso non sia possibile ottenere tali dati, ovvero se non esiste un piano corrispondente al valore del parametro ‘plan’ (per esempio se si digita nella barra degli indirizzi http://localhost:4200/tariffe/silver), usiamo l’istanza del servizio Router per redirigere l’utente verso la pagina 404.

<!-- plan-detail.component.html -->
<div>
  <h2>
    Piano {{ plan.name | titlecase }}
    <span class="badge badge-pill badge-{{ plan.colorClass }}">
      {{ plan.price | number:'1.0-2' }} &#8364;/mese
    </span>
  </h2>
  <h3 class="mt-3">Dettagli</h3>
  <ul class="list-group">
    <li class="list-group-item" *ngFor="let detail of plan.details">
      {{ detail }}
    </li>
  </ul>
</div>
<div class="mt-5">
  <a routerLink="/tariffe">Torna all'elenco delle tariffe</a>
</div>

Possiamo quindi lanciare la nostra applicazione con il comando ng serve e visualizzare il risultato ottenuto nel browser.

A questo punto supponiamo però di voler aggiungere alcuni link alle altre offerte disponibili alla fine della pagina dei dettagli. Per il momento, aggiungiamo manualmente solo un link alla pagina del piano standard e vediamo cosa succede se clicchiamo su quest’ultimo mentre siamo nella pagina dei dettagli del piano base. Ci aspettiamo che il percorso nella barra degli indirizzi cambi opportunamente (da ‘/tariffe/base’ a ‘/tariffe/standard’) e contemporaneamente che vengano mostrati i dati relativi al piano standard al posto di quelli del piano base.

Come possiamo vedere dal breve video riportato sopra, il comportamento reale non è quello atteso. La nostra applicazione sembra non funzionare correttamente ed il motivo è da ricondurre al fatto che all’interno del componente PlanDetailComponent abbiamo utilizzato la proprietà activatedRoute.snapshot per ottenere il valore del parametro del percorso corrente. Il problema è che in questo modo abbiamo ottenuto solo il valore del parametro ‘plan’ durante l’inizializzazione del componente, ma dal momento che ora stiamo riutilizzando il componente PlanDetailComponent dopo il cambio del percorso, quest’ultimo non ha modo di conoscere il valore aggiornato del parametro ‘plan’ in quanto nel codice corrente otteniamo tale informazione una sola volta all’interno del lifecycle hook ngOnInit() che viene eseguito solo durante la fase di inizializzazione del componente.

Per risolvere il problema appena descritto basta sostituire activatedRoute.snapshot con activatedRoute.paramMap che è un riferimento ad un Observable il quale emette un oggetto di tipo ParamMap. Quest’ultimo consente di accedere ai parametri del percorso corrente. Visto che ora lavoriamo con un Observable, il componente PlanDetailComponent eseguirà il metodo subscribe() sempre nel metodo ngOnInit(), ma ora riceverà una notifica ogni volta che l’Observable emette un nuovo valore, ovvero ogni volta che il parametro ‘plan’ e quindi l’indirizzo cambiano.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
import { Observable, Subscription } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';

import { PlanService } from './../plan.service';
import { Plan } from '../plan.model';

@Component({
  selector: 'simple-plan-detail',
  templateUrl: './plan-detail.component.html',
  styleUrls: ['./plan-detail.component.css']
})
export class PlanDetailComponent implements OnInit, OnDestroy {

  plan: Plan;
  otherPlansNames: string[];
  plan$: Observable<Plan[]>;
  planParam: string;
  planSubscription: Subscription;

  constructor(
    private planService: PlanService,
    private router: Router,
    private activatedRoute: ActivatedRoute
  ) { }

  ngOnInit() {
    this.plan$ = this.activatedRoute.paramMap.pipe(
      tap((params: ParamMap) => this.planParam = params.get('plan')),
      switchMap((params: ParamMap) =>
        this.planService.getPlans()
      )
    );

    this.planSubscription = this.plan$.subscribe((plans: Plan[]) => {
      this.plan = plans.find((plan) => plan.name === this.planParam);

      if (!this.plan) {
        this.router.navigate(['/404']);
      }

      this.otherPlansNames = plans.reduce(
        (arr: string[], currentPlan: Plan) => {
          if (currentPlan.name !== this.planParam) {
            arr.push(currentPlan.name);
          }
          return arr;
        }, []);
    });
  }

  ngOnDestroy() {
    this.planSubscription.unsubscribe();
  }
}

Ora nel metodo ngOnInit() accediamo alla proprietà activatedRoute.paramMap che restituisce un Observable<ParamMap>. Grazie al metodo pipe() preleviamo, attraverso l’operatore tap(), il valore del parametro del percorso corrente. Usiamo quindi l’operatore SwitchMap in quanto recuperiamo con il metodo this.planService.getPlans() un Observable che emette un array di oggetti di tipo Plan. È necessario usare un operatore come SwitchMap e non semplicemente map() perché this.planService.getPlans() restituisce un Observable. Se usassimo map() otterremo un oggetto di tipo Observable<Observable<Plan[]>>. In questo modo invece assegniamo a this.plan$ un oggetto di tipo Observable<Plan[]>.

All’interno del metodo plan$.subscribe() assegniamo alla proprietà plan le informazioni relative al piano tariffario corrispondente al paramentro ‘:plan’ del percorso corrente. Se quest’ultimo non è corretto redirigiamo l’utente verso la pagina 404. In caso contrario otteniamo un array contenente i nomi degli altri piani tariffari che inseriremo come link alla fine della pagina.

<div>
  <h2>
    Piano {{ plan.name | titlecase }}
    <span class="badge badge-pill badge-{{ plan.colorClass }}">
      {{ plan.price | number:'1.0-2' }} &#8364;/mese
    </span>
  </h2>
  <h3 class="mt-3">Dettagli Tariffa</h3>
  <ul class="list-group">
    <li 
      class="list-group-item" 
      *ngFor="let detail of plan.details">
        {{ detail }}
    </li>
  </ul>
</div>
<div class="mt-5">
  <h3>Altre tariffe</h3>
  <ul>
    <li *ngFor="let otherPlanName of otherPlansNames">
      <a 
        routerLink="/tariffe/{{ otherPlanName }}">
          Piano {{ otherPlanName | titlecase }}
      </a>
    </li>
  </ul>
</div>
<div class="mt-5">
  <a routerLink="/tariffe">Torna all'elenco delle tariffe</a>
</div>

Potete provare la versione corrente dell’applicazione al seguente link su stackblitz.

Creare un ‘feature module’ per i depositi

Concludiamo la nostra applicazione, aggiungendo un modulo anche per il percorso ‘/depositi’. Al fine di descrivere altre caratteristiche di Angular Router, questo nuovo modulo sarà caricato a richiesta (Lazy Loaded) solo nel momento in cui si visita la sezione dell’applicazione riguardante i depositi. Come è possibile vedere nel video riportato sotto, all’interno del pannello ‘Network’ viene effettuata una nuova richiesta e viene caricato il nuovo modulo soltanto se l’utente attiva un link all’area ‘/depositi’.

Per prima cosa rimuoviamo la cartella locations-list e al suo posto creiamo una nuova cartella warehouses in cui raggruppiamo tutti i file relativi al nuovo modulo. Useremo infatti un componente base WarehousesComponent, che sarà mostrato in corrispondenza del percorso ‘/depositi’, all’interno del quale impiegheremo di nuovo la direttiva RouterOutlet per creare un nuovo slot in cui finiranno due possibili componenti, WarehouseNotSelectedComponent o WarehouseDetailComponent, per i quali definiamo dei percorsi figli nella configurazione dei percorsi presente nel file warehouse-routing.module.ts.

Applicazione Angular MyBox schema di funzionamento con percorsi annidati

Vediamo quindi come creare tutti i file necessari in modo da avere alla fine un’applicazione con la seguente struttura.

tree src/app
src/app
├── app-routing.module.ts
├── app.component.css
├── app.component.html
├── app.component.ts
├── app.module.ts
├── home
│   ├── home.component.html
│   └── home.component.ts
├── navbar
│   ├── navbar.component.html
│   └── navbar.component.ts
├── page-not-found
│   ├── page-not-found.component.html
│   └── page-not-found.component.ts
├── plans
│   ├── plan-detail
│   │   ├── plan-detail.component.css
│   │   ├── plan-detail.component.html
│   │   └── plan-detail.component.ts
│   ├── plan-list
│   │   ├── plan-list.component.css
│   │   ├── plan-list.component.html
│   │   └── plan-list.component.ts
│   ├── plan.model.ts
│   ├── plan.service.ts
│   ├── plans-mock.ts
│   ├── plans-routing.module.ts
│   └── plans.module.ts
└── warehouses
    ├── warehouse-detail
    │   ├── warehouse-detail.component.css
    │   ├── warehouse-detail.component.html
    │   └── warehouse-detail.component.ts
    ├── warehouse-list
    │   ├── warehouse-list-item
    │   │   ├── warehouse-list-item.component.css
    │   │   ├── warehouse-list-item.component.html
    │   │   └── warehouse-list-item.component.ts
    │   ├── warehouse-list.component.css
    │   ├── warehouse-list.component.html
    │   └── warehouse-list.component.ts
    ├── warehouse-not-selected
    │   ├── warehouse-not-selected.component.css
    │   ├── warehouse-not-selected.component.html
    │   └── warehouse-not-selected.component.ts
    ├── warehouse.model.ts
    ├── warehouse.service.ts
    ├── warehouses
    │   ├── warehouses.component.css
    │   ├── warehouses.component.html
    │   └── warehouses.component.ts
    ├── warehouses-mock.ts
    ├── warehouses-routing.module.ts
    ├── warehouses-service.module.ts
    └── warehouses.module.ts

12 directories, 43 files

Come possiamo vedere dal codice riportato sopra, creiamo un nuovo modulo.

ng generate module warehouses/warehouses --flat --routing    
CREATE src/app/warehouses/warehouses-routing.module.ts (254 bytes)
CREATE src/app/warehouses/warehouses.module.ts (299 bytes)

È importante sottolineare che il modulo WarehousesModule non sarà aggiunto all’array imports del modulo AppModule visto che sarà caricato solo a richiesta. (Lazy Loaded)

E sempre per questo motivo dovremo invece modificare il file app-routing.module.ts per indicare che vogliamo caricare il suddetto modulo solo se necessario.

import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';

import { HomeComponent } from './home/home.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  // chiediamo ad Angular di caricare il modulo solo se necessario
  // utilizzando la proprietà 'loadChildren'
  { path: 'depositi', loadChildren: './warehouses/warehouses.module#WarehousesModule' },
  { path: '**', component: PageNotFoundComponent }
];

const routerModuleWithProviders: ModuleWithProviders<RouterModule> =
  RouterModule.forRoot(
    routes,
    { enableTracing: true } // <-- debugging purposes only
  );

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    routerModuleWithProviders
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule { }

Come mostrato nel frammento di codice riportato sopra, per i percorsi che iniziano con ‘/depositi’ carichiamo il modulo WarehousesModule a richiesta utilizzando una particolare forma sintattica in cui specifichiamo il percorso al file in cui è definito il modulo seguito dal carattere ‘#’ e dal nome della classe.

Prima di procedere con gli altri file e con la configurazione del Router, creiamo i file warehouse-mock.ts ,warehouse.model.ts in maniera simile a quanto già visto per i piani tariffari.

// warehouse.model.ts
export interface Warehouse {
  id: number;
  name: string;
  slug: string,
  address: {
    street: string,
    postalCode: string,
    city: string,
    country: string,
    countryCode: string
  };
}

export type Warehouses = Warehouse[];

All’interno del file warehouse.model.ts definiamo l’interfaccia Warehouse per gli oggetti contenenti le informazioni su ciascun deposito. Per comodità esportiamo anche il tipo Warehouses che altro non è se non un sinonimo di Warehouse[].

Creiamo quindi il file warehouse-mock.ts con dei dati fittizi sui depositi che useremo per semplicità nel corso dell’applicazione.

import { Warehouse, Warehouses } from './warehouse.model';

export const WAREHOUSES: Warehouses = [
  {
    id: 0,
    name: 'deposito a',
    slug: 'deposito-a',
    address: {
      street: 'via Bianchi, 1',
      postalCode: '20900',
      city: 'Torino',
      country: 'Italia',
      countryCode: 'it'
    }
  },
  {
    id: 1,
    name: 'deposito b',
    slug: 'deposito-b',
    address: {
      street: 'via Verdi, 2',
      postalCode: '52100',
      city: 'Milano',
      country: 'Italia',
      countryCode: 'it'
    }
  },
  {
    id: 2,
    name: 'deposito c',
    slug: 'deposito-c',
    address: {
      street: 'via Rossi, 3',
      postalCode: '60200',
      city: 'Roma',
      country: 'Italia',
      countryCode: 'it'
    }
  },
  {
    id: 3,
    name: 'deposito d',
    slug: 'deposito-d',
    address: {
      street: 'via Neri, 4',
      postalCode: '76300',
      city: 'Napoli',
      country: 'Italia',
      countryCode: 'it'
    }
  }
]

I componenti del modulo useranno il servizio WarehouseService per poter accedere ai dati presenti in warehouse-mock.ts.

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';

import { Warehouse, Warehouses } from './warehouse.model';
import { WAREHOUSES } from './warehouses-mock';
import { WarehousesServiceModule } from './warehouses-service.module';

@Injectable({
  providedIn: WarehousesServiceModule
})
export class WarehouseService {

  getWarehouses(): Observable<Warehouses> {
    return of(WAREHOUSES);
  }

  getWarehouse(warehouseSlug: string) {
    return this.getWarehouses().pipe(
      map((warehouses: Warehouses) =>
          warehouses.find((warehouse: Warehouse) =>
              warehouse.slug === warehouseSlug
          )
      )
    );
  }
}

Il servizio WarehouseService presenta due metodi che restituiscono rispettivamente tutto l’array dei depositi definito in warehouse.model.ts o un singolo deposito la cui proprietà slug è uguale al parametro warehouseSlug. L’aspetto più interessante di questo servizio è che dal momento che WarehousesModule viene caricato a richiesta, possiamo limitare anche la visibilità del servizio a quest’ultimo. Nel frammento di codice riportato sopra però alla proprietà ‘providedIn’ dell’oggetto di metadati del decoratore @Injectable abbiamo assegnato come valore la classe WarehousesServiceModule corrispondente al modulo definito nel file warehouses-service.module.ts che abbiamo creato col comando ng generate module warehouses/warehouses-service --flat e che importeremo in WarehousesModule.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

@NgModule({
  declarations: [],
  imports: [
    CommonModule
  ]
})
export class WarehousesServiceModule { }

Il motivo per cui creiamo un altro modulo di supporto è dovuto al fatto che se avessimo usato direttamente il modulo WarehousesModule come valore della proprietà ‘providedIn’ del decoratore di WarehouseService, iniettando il servizio in un componente, Angular avrebbe mostrato un messaggio come quello riportato sotto per segnalare il verificarsi di una dipendenza circolare.

WARNING in Circular dependency detected:
src/app/warehouses/warehouse-list/warehouse-list.component.ts -> 
src/app/warehouses/warehouse.service.ts -> 
src/app/warehouses/warehouses.module.ts -> s
rc/app/warehouses/warehouse-list/warehouse-list.component.ts

WARNING in Circular dependency detected:
src/app/warehouses/warehouse.service.ts -> 
src/app/warehouses/warehouses.module.ts -> 
src/app/warehouses/warehouse-list/warehouse-list.component.ts -> 
src/app/warehouses/warehouse.service.ts

WARNING in Circular dependency detected:
src/app/warehouses/warehouses.module.ts -> 
src/app/warehouses/warehouse-list/warehouse-list.component.ts -> 
src/app/warehouses/warehouse.service.ts -> 
src/app/warehouses/warehouses.module.ts

Invece, introducendo semplicemente un modulo di supporto WarehousesServiceModule, riusciamo a limitare la visibilità del servizio al solo modulo caricato a richiesta evitando che venga inserito nel bundle dell’applicazione caricato in fase di avvio. In più grazie al meccanismo del Tree-Shaking il servizio non verrà scartato solo se è effettivamente utilizzato da qualche componente.

Come accennato sopra, il modulo WarehousesServiceModule sarà poi importato nel modulo WarehousesModule in cui importiamo anche WarehousesRoutingModule ed elenchiamo nell’array ‘declarations’ tutti i componenti del modulo che andremo a descrivere a breve.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { WarehousesRoutingModule } from './warehouses-routing.module';
import { WarehouseListComponent } from './warehouse-list/warehouse-list.component';
import { WarehousesComponent } from './warehouses/warehouses.component';
import { WarehouseNotSelectedComponent } from './warehouse-not-selected/warehouse-not-selected.component';
import { WarehouseDetailComponent } from './warehouse-detail/warehouse-detail.component';
import { WarehouseListItemComponent } from './warehouse-list/warehouse-list-item/warehouse-list-item.component';
import { WarehousesServiceModule } from './warehouses-service.module';

@NgModule({
  declarations: [
    WarehouseListComponent,
    WarehousesComponent,
    WarehouseNotSelectedComponent,
    WarehouseDetailComponent,
    WarehouseListItemComponent
  ],
  imports: [
    CommonModule,
    WarehousesServiceModule,
    WarehousesRoutingModule
  ]
})
export class WarehousesModule { }

Prima di procedere all’analisi dei singoli componenti, vediamo il contenuto del file warehouses-routing.module.ts.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { WarehousesComponent } from './warehouses/warehouses.component';
import { WarehouseNotSelectedComponent } from './warehouse-not-selected/warehouse-not-selected.component';
import { WarehouseDetailComponent } from './warehouse-detail/warehouse-detail.component';

const warehouseRoutes: Routes =  [
  { path: '', component: WarehousesComponent, children: [
    { path: '', component: WarehouseNotSelectedComponent },
    { path: ':slug', component: WarehouseDetailComponent },
  ] },
];

@NgModule({
  imports: [RouterModule.forChild(warehouseRoutes)],
  exports: [RouterModule]
})
export class WarehousesRoutingModule { }

Dal frammento di codice riportato sopra vediamo che abbiamo definito un array warehouseRoutes con una serie di percorsi che sono relativi al percorso base ‘/depositi’ per il quale all’interno del file app-routing.module.ts abbiamo indicato la volontà di caricare il modulo WarehousesModule. Quindi quando nella barra degli indirizzi sarà specificato un percorso che inizia con ‘/depositi’, verrà sempre caricato il componente WarehousesComponent nello slot <router-outlet> presente nel template del componente AppComponent.

Nel template di WarehousesComponent abbiamo però a sua volta inserito uno slot annidato <router-outlet> nel quale saranno caricati, a seconda del percorso dell’indirizzo, i componenti elencati nell’array children dell’oggetto di posizione zero dell’array warehouseRoutes presente nel codice sorgente relativo al file warehouses-routing.module.ts.

Se il percorso è semplicemente ‘/depositi’, allora viene caricato il componente WarehouseNotSelectedComponent, se invece viene specificato un paramentro che chiamiamo ‘:slug’ (avremo un percorso del tipo ‘/depositi/deposito-a’ con il parametro ‘slug’ pari a ‘deposito-a’), allora sarà attivato il componente WarehouseDetailComponent con i dettagli di un certo deposito.

<!-- warehouses.component.html -->
<div class="row">
  <div class="col-md-3">
    <simple-warehouse-list></simple-warehouse-list>
  </div>
  <div class="col-md-9">
    <router-outlet></router-outlet>
  </div>
</div>

Come possiamo notare dal codice del file warehouses.component.html, oltre a mostrare un componente in maniera dinamica, il componente WarehousesComponent presenta sempre la lista dei depositi in cui ogni singolo elemento sarà un link che attiva il componente WarehouseDetailComponent il quale mostrerà le opportune informazioni.

Tornando di nuovo per un momento al file warehouses-routing.module.ts, notiamo che anche in questo caso useremo il metodo RouterModule.forChild() per registrare i percorsi del modulo secondario con la singola istanza del Router dell’applicazione.

@NgModule({
  imports: [RouterModule.forChild(warehouseRoutes)],
  exports: [RouterModule]
})

Per quanto riguarda quindi gli altri componenti del modulo, WarehouseNotSelectedComponent, così come WarehousesComponent, non presenta nessun metodo o proprietà rilevante nella definizione del componente contenuta nel file con estensione .ts. Il file warehouse-not-selected.component.html avrà solo il seguente paragrafo.

<p>
  Seleziona un deposito dall'elenco per maggiori informazioni
</p>

Di maggiore interesse sono invece i tre restanti componenti. WarehouseListComponent si occupa di prelevare la lista dei depositi grazie al servizio WarehouseService di cui invoca il metodo getWarehouses() in fase di inizializzazione del componente.

import { ActivatedRoute, ParamMap } from '@angular/router';
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';

import { Warehouses } from './../warehouse.model';
import { WarehouseService } from './../warehouse.service';
@Component({
  selector: 'simple-warehouse-list',
  templateUrl: './warehouse-list.component.html',
  styleUrls: ['./warehouse-list.component.css']
})
export class WarehouseListComponent implements OnInit {

  warehouse$: Observable<Warehouses>;

  constructor(
    private warehouseService: WarehouseService
  ) { }

  ngOnInit() {
    this.warehouse$ = this.warehouseService.getWarehouses();
  }

}

Ottenuto un riferimento all’Observable che emette l’array contenente le informazioni sui depositi, ci affidiamo alla pipe async per gestire la sottoscrizione e la cancellazione all’interno del template.

<!-- warehouse-list.component.html -->
<div class="list-group">
  <simple-warehouse-list-item
    *ngFor="let warehouse of warehouse$ | async"  [warehouse]="warehouse">
  </simple-warehouse-list-item>
</div>

Notiamo che a ciascun WarehouseListItemComponent passiamo l’intero oggetto con le informazioni su un deposito sfruttando la sua proprietà di input warehouse.

import { Subscription } from 'rxjs';
import { Router, NavigationEnd } from '@angular/router';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Input } from '@angular/core';

import { Warehouse } from './../../warehouse.model';

@Component({
  selector: 'simple-warehouse-list-item',
  templateUrl: './warehouse-list-item.component.html',
  styleUrls: ['./warehouse-list-item.component.css']
})

export class WarehouseListItemComponent implements OnInit, OnDestroy {

  @Input() warehouse: Warehouse;
  currentWarehouseName: string;
  subscription: Subscription;

  constructor(private router: Router) { }

  ngOnInit() {
    this.subscription = this.router.events.subscribe(event => {
      if (event instanceof NavigationEnd) {
        this.currentWarehouseName = event.url.split('/').pop();
      }
    });
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }

}

All’interno di WarehouseListItemComponent sfruttiamo uno degli eventi del ciclo di vita del router, ovvero NavigationEnd, per determinare se è stato selezionato un deposito da parte dell’utente. Infatti, in questo caso nella barra degli indirizzi avremo un percorso del tipo /depositi/deposito-a. Utilizzando il metodo String.prototype.split() con argomento il separatore ‘/’ otteniamo l’array ["", "depositi", "deposito-a"] di cui estraiamo l’ultimo elemento (‘deposito-a’) con il metodo Array.prototype.pop() e lo assegniamo alla proprietà currentWarehouseName. Nel metodo ngOnDestroy() ci assicuriamo di cancellare la sottoscrizione all’Observable.

<!-- warehouse-list-item.component.html -->
<a
  [routerLink]="warehouse.slug"
  class="list-group-item list-group-item-action"
  [class.active]="warehouse.slug === currentWarehouseName">
  {{ warehouse.name | titlecase }}
</a>

Ciascun elemento della lista conterrà dunque un link che permette di attivare il componente WarehouseDetailComponent il quale mostrerà le opportune informazioni. Avrà inoltre una classe CSS ‘active’ quando l’utente lo seleziona.

Infine il componente WarehouseDetailComponent si occupa di mostrare le informazioni relative al deposito selezionato estraendo il valore del parametro ‘slug’ dall’Observable assegnato alla proprietà paramMap di activatedRoute ed invocando il metodo warehouseService.getWarehouse() in maniera simile a quanto visto in precedenza.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Subscription, Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';

import { WarehouseService } from './../warehouse.service';
import { Warehouse } from './../warehouse.model';

@Component({
  selector: 'simple-warehouse-detail',
  templateUrl: './warehouse-detail.component.html',
  styleUrls: ['./warehouse-detail.component.css']
})
export class WarehouseDetailComponent implements OnInit, OnDestroy {

  warehouseDetails: Warehouse;
  warehouse$: Observable<Warehouse>;
  observableSubscription: Subscription;

  constructor(
    private warehouseService: WarehouseService,
    private activatedRoute: ActivatedRoute
  ) { }

  ngOnInit() {
    this.warehouse$ = this.activatedRoute.paramMap.pipe(
      switchMap(
        (paramMap: ParamMap) => this.warehouseService.getWarehouse(paramMap.get('slug'))
      )
    );

    this.observableSubscription = this.warehouse$.subscribe(
      (warehouse: Warehouse) => this.warehouseDetails = warehouse
    );
  }

  ngOnDestroy(): void {
    this.observableSubscription.unsubscribe();
  }

}
<div >
  <h2>{{ warehouseDetails.name | titlecase }}</h2>
  <address>
    {{ warehouseDetails.address.street }} <br>
    {{ warehouseDetails.address.postalCode }} -
    {{ warehouseDetails.address.city }}
    ({{ warehouseDetails.address.countryCode | uppercase}})
  </address>
</div>

Potete provare l’esempio completo descritto in questa lezione su Stackblitz

Conclusioni

In quest’ultima lezione abbiamo introdotto Angular Router e abbiamo visto un esempio pratico per dimostrare quanto sia semplice creare una SPA (Single Page Application) con più view in Angular. Concludiamo così questa guida introduttiva ad Angular. A questo punto abbiamo raggiunto una familiarità tale con Angular da poter iniziare a realizzare delle applicazioni abbastanza complete.

Pubblicitร