Queste sono le note della lezione del corso su Angular. Siamo allâultima parte del corso, quella in cui i pezzi si incastrano: ricostruiamo la pagina del menu di Awesome Pizza con un framework, e nel farlo useremo tutto quello che abbiamo visto finora. LâHTML vive nei template, lâSCSS negli stili dei componenti, e TypeScript dappertutto, perchĂŠ Angular non vi dĂ nemmeno la scelta: si scrive in TypeScript e basta.
Prima una domanda legittima: perchĂŠ un framework? Finora la nostra pagina era statica. Appena i dati iniziano a cambiare, un ordine aggiunto al carrello, un menu caricato dal server, tocca aggiornare il DOM a mano con JavaScript, e su unâapplicazione vera questo approccio non scala: ogni pezzo di interfaccia va tenuto sincronizzato con i dati, a mano, per sempre. I framework ribaltano il problema. Noi descriviamo come lâinterfaccia deve apparire dati certi dati, e a tenerla sincronizzata pensa il framework.
La versione di riferimento è Angular 16, lâultima al momento della lezione.
Il primo progetto
Angular si usa attraverso la sua CLI, che si installa con npm e genera il progetto:
npm install -g @angular/cli
ng new awesome-pizza --style=scss
cd awesome-pizza
ng serve
Lâopzione --style=scss dice alla CLI che gli stili dei componenti saranno file SCSS: la lezione della settimana scorsa non è andata sprecata. ng serve avvia il server di sviluppo su http://localhost:4200, con ricaricamento automatico a ogni salvataggio.
La CLI genera parecchi file di configurazione di cui per ora non ci occupiamo. Quello che ci interessa sta tutto in src/app/.
NgModule
Il punto di ingresso dellâapplicazione è app.module.ts:
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { AppComponent } from "./app.component";
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Un NgModule è il registro dellâapplicazione: declarations elenca i componenti che ci appartengono, imports gli altri moduli di cui abbiamo bisogno, bootstrap il componente radice da cui parte tutto. Ogni componente che creeremo andrĂ dichiarato qui, e quando un template non riconosce un componente la prima cosa da controllare è proprio questa lista.
Una nota per onestà sul futuro: da Angular 15 esistono i componenti standalone, che si dichiarano da soli e rendono gli NgModule opzionali, e la direzione del framework è chiaramente quella. I progetti che incontrerete oggi però sono fatti di NgModule, e gli NgModule resteranno nelle codebase per anni, quindi in questa lezione usiamo quelli. Quando leggerete standalone: true in un componente, sappiate che è la stessa cosa senza il registro.
Componenti
Il componente è la cellula di unâapplicazione Angular: un pezzo di interfaccia autonomo e riusabile, fatto di una classe TypeScript, un template HTML e un foglio di stile, ognuno nel suo file. La CLI li genera per noi:
ng generate component pizza-card
Il decoratore @Component lega i tre file:
import { Component } from "@angular/core";
@Component({
selector: "app-pizza-card",
templateUrl: "./pizza-card.component.html",
styleUrls: ["./pizza-card.component.scss"],
})
export class PizzaCardComponent {}
selector è il nome del tag con cui il componente si usa negli altri template: <app-pizza-card>, come fosse un tag HTML inventato da noi. La classe contiene i dati e la logica, il template li mostra. Vediamo come.
Il template: dove HTML incontra i dati
Riprendiamo il tipo Pizza della lezione su TypeScript, in un file models/pizza.ts:
export type Pizza = {
nome: string;
prezzo: number;
ingredienti: string[];
sconto?: number;
};
Il componente radice tiene lâelenco delle pizze e il contatore del carrello:
import { Component } from "@angular/core";
import { Pizza } from "./models/pizza";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],
})
export class AppComponent {
carrello = 0;
pizze: Pizza[] = [
{
nome: "Margherita",
prezzo: 9,
ingredienti: ["Farina", "Pomodoro", "Mozzarella"],
},
{
nome: "Marinara",
prezzo: 5,
ingredienti: ["Farina", "Pomodoro", "Aglio", "Origano"],
},
{
nome: "Contadina",
prezzo: 19,
ingredienti: ["Farina", "Melanzane", "Peperoni", "Mozzarella"],
},
];
}
E il template li usa. La struttura è lâHTML della lezione sullâHTML, con qualche superpotere in piĂš:
<header>
<h1>Awesome Pizza</h1>
</header>
<main class="menu">
<app-pizza-card
*ngFor="let pizza of pizze"
[pizza]="pizza"
(ordinata)="aggiungiAlCarrello($event)"
></app-pizza-card>
</main>
<footer>
<p>Pizze nel carrello: {{ carrello }}</p>
</footer>
I superpoteri, uno per uno:
{{ carrello }}è lâinterpolazione: il valore della proprietĂ finisce nel testo, e quando cambia il framework aggiorna il DOM da solo. Questo è il contratto di cui parlavamo allâinizio.*ngFor="let pizza of pizze"ripete lâelemento per ogni pizza dellâarray. Ă ilfor ofdella lezione su JavaScript in versione dichiarativa: invece di scrivere il ciclo, dichiariamo che questo pezzo di template si ripete.[pizza]="pizza"è il property binding: passa un dato al componente figlio. Le parentesi quadre dicono âvaluta lâespressioneâ, senza sarebbe la stringa letterale âpizzaâ.(ordinata)="aggiungiAlCarrello($event)"è lâevent binding: quando il figlio emette lâeventoordinata, viene chiamato il nostro metodo.
Input e Output
La card riceve la sua pizza e comunica al genitore che lâutente ha ordinato. In Angular questo dialogo ha una grammatica precisa: i dati scendono con @Input, gli eventi salgono con @Output.
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Pizza } from "../models/pizza";
@Component({
selector: "app-pizza-card",
templateUrl: "./pizza-card.component.html",
styleUrls: ["./pizza-card.component.scss"],
})
export class PizzaCardComponent {
@Input() pizza!: Pizza;
@Output() ordinata = new EventEmitter<Pizza>();
ordina() {
this.ordinata.emit(this.pizza);
}
}
Il template della card è la card della lezione su CSS, con i dati al posto dei valori fissi:
<article class="card">
<h3>{{ pizza.nome }}</h3>
<p>{{ pizza.ingredienti.join(", ") }}</p>
<p class="prezzo">{{ pizza.prezzo }}âŹ</p>
<p *ngIf="pizza.sconto">In offerta: -{{ pizza.sconto }}âŹ</p>
<button (click)="ordina()">Ordina</button>
</article>
Qui compare *ngIf: lâelemento esiste solo se la condizione è vera. La riga dellâofferta compare solo per le pizze che hanno uno sconto.
Nel genitore manca solo il metodo che riceve lâevento:
aggiungiAlCarrello(pizza: Pizza) {
this.carrello = this.carrello + 1;
console.log(`Ordinata una ${pizza.nome}`);
}
Cliccate Ordina e guardate il contatore nel footer aggiornarsi. Nessuno ha toccato il DOM: abbiamo cambiato un numero in una classe, il resto è successo da solo.
Servizi e dependency injection
Lâarray delle pizze dentro al componente è una scorciatoia che ci siamo concessi per arrivare fin qui. I dati e la logica di accesso ai dati non vivono nei componenti, che devono occuparsi di interfaccia: vivono nei servizi.
import { Injectable } from "@angular/core";
import { Pizza } from "./models/pizza";
@Injectable({ providedIn: "root" })
export class MenuService {
getPizze(): Pizza[] {
return [
{
nome: "Margherita",
prezzo: 9,
ingredienti: ["Farina", "Pomodoro", "Mozzarella"],
},
{
nome: "Marinara",
prezzo: 5,
ingredienti: ["Farina", "Pomodoro", "Aglio", "Origano"],
},
{
nome: "Contadina",
prezzo: 19,
ingredienti: ["Farina", "Melanzane", "Peperoni", "Mozzarella"],
},
];
}
}
Il componente non crea il servizio: lo chiede nel costruttore, e Angular glielo fornisce. Il meccanismo si chiama dependency injection ed è uno dei pilastri del framework.
import { Component, OnInit } from "@angular/core";
import { MenuService } from "./menu.service";
import { Pizza } from "./models/pizza";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],
})
export class AppComponent implements OnInit {
carrello = 0;
pizze: Pizza[] = [];
constructor(private menuService: MenuService) {}
ngOnInit() {
this.pizze = this.menuService.getPizze();
}
}
ngOnInit è un hook del ciclo di vita: viene chiamato quando il componente è pronto, ed è il posto giusto per caricare i dati iniziali. Per ora il vantaggio del servizio sembra solo ordine; diventa concreto nella prossima sezione.
Caricare il menu dalle API
Nel mondo reale il menu arriva dal backend. Serve HttpClientModule, da aggiungere agli imports del modulo:
// app.module.ts
import { HttpClientModule } from "@angular/common/http";
@NgModule({
declarations: [AppComponent, PizzaCardComponent],
imports: [BrowserModule, HttpClientModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
E il servizio cambia implementazione, iniettando a sua volta HttpClient:
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Pizza } from "./models/pizza";
@Injectable({ providedIn: "root" })
export class MenuService {
constructor(private http: HttpClient) {}
getPizze() {
return this.http.get<Pizza[]>("https://api.awesomepizza.com/pizzas");
}
}
Notate il get<Pizza[]>: è lo stesso identico discorso della getJson<T> vista nella lezione su TypeScript, generics compresi.
Câè però una differenza importante: http.get non restituisce una Promise ma un Observable, un oggetto del mondo RxJS che rappresenta un flusso di valori nel tempo. RxJS merita una lezione tutta sua; per oggi vi basta il metodo subscribe, che consegna il valore quando arriva:
ngOnInit() {
this.menuService.getPizze().subscribe(pizze => {
this.pizze = pizze;
});
}
Il componente, e questo è il punto, non è cambiato di una virgola nel suo rapporto col servizio: chiede le pizze, le riceve. Da dove arrivino è un dettaglio del servizio.
Ultima cosa, e poi vi lascio andare: la prima volta che farete questa chiamata verso un backend vero, câè unâottima probabilitĂ che il browser la blocchi con un errore rosso che ormai sapete leggere. Se non lo sapete leggere, la lezione su CORS è lĂŹ apposta.
Nella prossima lezione costruiamo questa stessa applicazione, identica, con React. Vi conviene tenere questo progetto a portata di mano: il confronto è il punto.
Per approfondire
- La documentazione ufficiale di Angular
- Tour of Heroes - il tutorial ufficiale, fatto bene
- RxJS - per quando vorrete capire davvero gli Observable