I Nuovi Template Angular: @if, @for, @defer
Con Angular 17 il team ha introdotto un nuovo sistema di control flow nei template
che sostituisce le storiche direttive strutturali *ngIf, *ngFor e
ngSwitch. La nuova sintassi con @if, @for,
@switch e @defer rappresenta un cambiamento fondamentale
nel modo di scrivere i template Angular: più leggibile, più performante e con funzionalità
completamente nuove come il lazy loading dichiarativo dei componenti.
In questo terzo articolo della serie Angular Moderno analizzeremo in profondità ogni costrutto: dalla sintassi base ai pattern avanzati, dalla migrazione automatica alle best practices consolidate dalla community. Se stai ancora usando le direttive strutturali classiche, questa guida ti mostrera perchè è il momento di migrare.
Panoramica della Serie Angular Moderno
| # | Articolo | Focus |
|---|---|---|
| 1 | Angular Signals | Reattività fine-grained |
| 2 | Zoneless Change Detection | Eliminare Zone.js |
| 3 | Sei qui - Nuovi Template @if, @for, @defer | Control flow moderno |
| 4 | Standalone Components | Architettura senza NgModule |
| 5 | Signal Forms | Form reattivi con Signals |
| 6 | SSR e Incremental Hydration | Server-Side Rendering |
| 7 | Core Web Vitals in Angular | Performance e metriche |
| 8 | Angular PWA | Progressive Web App |
| 9 | Dependency Injection Avanzata | DI tree-shakable |
| 10 | Migrare da Angular 17 a 21 | Guida alla migrazione |
perchè una Nuova Sintassi? I Limiti delle Direttive Strutturali
Le direttive strutturali *ngIf, *ngFor e ngSwitch
hanno servito Angular per quasi un decennio, ma presentano limiti strutturali che
la nuova sintassi risolve completamente.
Problemi delle Direttive Strutturali Classiche
- Microsintassi opaca: Espressioni come
*ngFor="let item of items; trackBy: trackFn; let i = index; let first = first"sono difficili da leggere e ricordare - Nessun else-if nativo: Con
*ngIfnon esiste unelse if: bisogna annidare più*ngIfo usareng-templateconelse - Peso nel bundle: Le direttive strutturali sono classi Angular importate dal modulo CommonModule, che devono essere incluse nel bundle
- Track implicito:
*ngFornon obbliga a definire una funzionetrackBy, causando problemi di performance silenziosi - Nessun lazy loading: Non esiste un meccanismo dichiarativo per caricare componenti on-demand direttamente dal template
- Verbosita: Per un semplice if/else servono
ng-templatecon variabili template reference, aumentando il boilerplate
Guardiamo un esempio concreto che mostra la differenza di leggibilità.
<!-- Vecchia sintassi: verbose e frammentata -->
<div *ngIf="user; else loadingTpl">
<h2>{{ user.name }}</h2>
<div *ngIf="user.role === 'admin'; else userTpl">
<span class="badge admin">Amministratore</span>
</div>
<ng-template #userTpl>
<span class="badge user">Utente</span>
</ng-template>
</div>
<ng-template #loadingTpl>
<div class="spinner">Caricamento...</div>
</ng-template>
<!-- Nuova sintassi: chiara e lineare -->
@if (user) {
<h2>{{ user.name }}</h2>
@if (user.role === 'admin') {
<span class="badge admin">Amministratore</span>
} @else {
<span class="badge user">Utente</span>
}
} @else {
<div class="spinner">Caricamento...</div>
}
Vantaggi della Nuova Sintassi in Sintesi
| Aspetto | Direttive Strutturali | Nuovo Control Flow |
|---|---|---|
| Importazioni necessarie | CommonModule o import singoli | Nessuna (built-in nel compiler) |
| Else-if | Non supportato nativamente | @else if nativo |
| Track obbligatorio | No (facoltativo) | Si (obbligatorio in @for) |
| Lazy loading | Non disponibile | @defer con trigger dichiarativi |
| Bundle size | Direttive incluse nel bundle | Zero overhead (compilato) |
| Type checking | Limitato | Completo |
| Leggibilita | Microsintassi complessa | Simile a JavaScript/TypeScript |
@if e @else: Rendering Condizionale Moderno
Il blocco @if sostituisce *ngIf con una sintassi che
ricorda le strutture di controllo di JavaScript. Supporta nativamente @else if
e @else, eliminando la necessità di ng-template.
Sintassi Base
<!-- Condizione semplice -->
@if (isLoggedIn()) {
<app-dashboard />
}
<!-- If con else -->
@if (user()) {
<h2>Benvenuto, {{ user().name }}</h2>
} @else {
<app-login-form />
}
<!-- If / else if / else -->
@if (status() === 'loading') {
<div class="spinner">Caricamento in corso...</div>
} @else if (status() === 'error') {
<div class="error">Si è verificato un errore</div>
} @else if (status() === 'empty') {
<div class="empty">Nessun risultato trovato</div>
} @else {
<app-data-table [data]="data()" />
}
Alias con la keyword "as"
Una funzionalità potente di @if e la possibilità di assegnare un
alias al risultato della condizione. Questo è particolarmente utile quando si lavora
con Signal o Observable che restituiscono valori nullable.
<!-- Salva il risultato della condizione in una variabile locale -->
@if (currentUser(); as user) {
<div class="profile">
<h3>{{ user.name }}</h3>
<p>Email: {{ user.email }}</p>
<p>Ruolo: {{ user.role }}</p>
</div>
}
<!-- Utile con pipe asincrone o trasformazioni -->
@if (users() | filterActive; as activeUsers) {
<p>Utenti attivi: {{ activeUsers.length }}</p>
}
Differenze chiave rispetto a *ngIf
@ifnon richiede nessuna importazione (non è una direttiva, è parte del compiler)@else ifè supportato nativamente, eliminando la necessità ding-templateannidati- La sintassi
asfunziona come aliasing del valore truthy della condizione - Il type narrowing funziona correttamente: dentro il blocco
@ifil tipo è già ristretto - Non serve più il
CommonModuleo l'import diretto diNgIf
@for con track: Iterazione Performante e Obbligatoria
Il blocco @for sostituisce *ngFor introducendo un cambiamento
fondamentale: l'espressione track è obbligatoria. Questa scelta di design
forza gli sviluppatori a definire come Angular identifica univocamente ogni elemento della
lista, prevenendo problemi di performance che con *ngFor passavano inosservati.
Sintassi Base
<!-- Iterazione base con track per id -->
@for (product of products(); track product.id) {
<div class="product-card">
<h3>{{ product.name }}</h3>
<p>{{ product.price | currency:'EUR' }}</p>
</div>
}
<!-- Con variabili implicite -->
@for (item of items(); track item.id; let i = $index; let isFirst = $first; let isLast = $last) {
<div class="item" [class.first]="isFirst" [class.last]="isLast">
<span class="index">{{ i + 1 }}.</span>
<span>{{ item.name }}</span>
</div>
}
<!-- Track per indice (per liste senza id univoco) -->
@for (label of labels(); track $index) {
<span class="tag">{{ label }}</span>
}
Variabili Implicite Disponibili in @for
| Variabile | Tipo | Descrizione |
|---|---|---|
$index | number | Indice corrente (0-based) |
$first | boolean | true se e il primo elemento |
$last | boolean | true se e l'ultimo elemento |
$even | boolean | true se l'indice e pari |
$odd | boolean | true se l'indice e dispari |
$count | number | Numero totale di elementi |
perchè track e Obbligatorio
Quando Angular aggiorna una lista, deve decidere quali elementi del DOM ricreare, quali
spostare e quali mantenere invariati. L'espressione track fornisce ad Angular
un'identità stabile per ogni elemento, permettendo aggiornamenti ottimali.
// Componente con lista aggiornata frequentemente
@Component({
selector: 'app-live-feed',
standalone: true,
template: `
<!-- CORRETTO: track per identità unica -->
<!-- Quando la lista cambia, Angular riusa i nodi DOM esistenti -->
@for (post of posts(); track post.id) {
<app-post-card [post]="post" />
}
<!-- SCONSIGLIATO: track per indice -->
<!-- Se un elemento viene aggiunto in cima, TUTTI i nodi vengono ricreati -->
@for (post of posts(); track $index) {
<app-post-card [post]="post" />
}
`
})
export class LiveFeedComponent {
posts = signal<Post[]>([]);
addNewPost(post: Post) {
// Con track post.id: solo 1 nodo DOM creato
// Con track $index: N nodi DOM ricreati
this.posts.update(list => [post, ...list]);
}
}
Quando usare $index come track
Usa track $index solo quando gli elementi della lista non hanno un'identità
stabile (ad esempio, un array di stringhe semplici). In tutti gli altri casi, preferisci
sempre una proprietà univoca come id. Usare $index su liste
che cambiano frequentemente è una delle cause principali di performance scadenti nelle
applicazioni Angular.
@switch: Selezione Multipla Pulita
Il blocco @switch sostituisce la combinazione di direttive
ngSwitch, *ngSwitchCase e *ngSwitchDefault
con una sintassi molto più naturale e leggibile.
<!-- PRIMA: ngSwitch (verboso, richiede contenitore) -->
<div [ngSwitch]="orderStatus">
<div *ngSwitchCase="'pending'">
<span class="badge warning">In Attesa</span>
</div>
<div *ngSwitchCase="'processing'">
<span class="badge info">In Lavorazione</span>
</div>
<div *ngSwitchCase="'shipped'">
<span class="badge success">Spedito</span>
</div>
<div *ngSwitchCase="'delivered'">
<span class="badge success">Consegnato</span>
</div>
<div *ngSwitchDefault>
<span class="badge">Sconosciuto</span>
</div>
</div>
<!-- DOPO: @switch (pulito, senza contenitore) -->
@switch (orderStatus()) {
@case ('pending') {
<span class="badge warning">In Attesa</span>
}
@case ('processing') {
<span class="badge info">In Lavorazione</span>
}
@case ('shipped') {
<span class="badge success">Spedito</span>
}
@case ('delivered') {
<span class="badge success">Consegnato</span>
}
@default {
<span class="badge">Sconosciuto</span>
}
}
Note su @switch
@switchusa il confronto stretto (===), identico a JavaScript- Non richiede un elemento contenitore: il confronto è direttamente sull'espressione
@defaultè opzionale ma consigliato per gestire casi imprevisti- Non supporta il fall-through: ogni
@caseè isolato (comportamento desiderabile) - A differenza di
ngSwitch, non servono importazioni dalCommonModule
@defer: Lazy Loading Dichiarativo
@defer è la vera rivoluzione introdotta con la nuova sintassi dei template.
Permette di caricare componenti, direttive e pipe on-demand direttamente
dal template, senza necessità di codice TypeScript per il lazy loading. Il compilatore
Angular automaticamente separa il codice referenziato dentro @defer
in chunk separati, scaricati solo quando il trigger specificato si attiva.
Sintassi Base e Blocchi Associati
@defer (on viewport) {
<!-- Contenuto caricato lazily -->
<app-heavy-chart [data]="chartData()" />
} @placeholder {
<!-- Mostrato prima che il caricamento inizi -->
<div class="chart-placeholder">
<p>Il grafico apparira qui</p>
</div>
} @loading (after 150ms; minimum 300ms) {
<!-- Mostrato durante il caricamento del chunk -->
<div class="spinner">Caricamento grafico...</div>
} @error {
<!-- Mostrato se il caricamento fallisce -->
<div class="error">Impossibile caricare il grafico</div>
}
Blocchi Associati a @defer
| Blocco | Scopo | Opzioni |
|---|---|---|
@placeholder |
Contenuto mostrato prima che il trigger si attivi | minimum: tempo minimo di visualizzazione |
@loading |
Contenuto mostrato durante il download del chunk | after: ritardo prima di mostrarlo; minimum: durata minima |
@error |
Contenuto mostrato se il caricamento fallisce | Nessuna opzione |
Trigger Disponibili
@defer supporta diversi trigger che determinano quando iniziare
il caricamento del contenuto lazy. Possono essere combinati con l'operatore logico
per creare condizioni di caricamento sofisticate.
<!-- 1. on viewport: carica quando l'elemento entra nel viewport -->
@defer (on viewport) {
<app-comments [postId]="postId()" />
} @placeholder {
<div style="height: 200px">Scorri per vedere i commenti</div>
}
<!-- 2. on idle: carica quando il browser è inattivo -->
@defer (on idle) {
<app-analytics-widget />
}
<!-- 3. on interaction: carica al click/focus su un elemento -->
<button #loadBtn>Mostra dettagli</button>
@defer (on interaction(loadBtn)) {
<app-product-details [id]="productId()" />
} @placeholder {
<p>Clicca il pulsante per vedere i dettagli</p>
}
<!-- 4. on hover: carica quando il mouse passa sopra un elemento -->
<div #previewArea>Passa il mouse per l'anteprima</div>
@defer (on hover(previewArea)) {
<app-image-preview [url]="imageUrl()" />
}
<!-- 5. on timer: carica dopo un intervallo di tempo -->
@defer (on timer(5s)) {
<app-newsletter-popup />
}
<!-- 6. when: carica quando una condizione diventa true -->
@defer (when isAuthenticated()) {
<app-admin-panel />
} @placeholder {
<p>Effettua il login per accedere al pannello admin</p>
}
<!-- 7. Combinazione di trigger -->
@defer (on viewport; on timer(10s)) {
<app-sidebar-ads />
} @placeholder {
<div class="ad-placeholder"></div>
}
Riepilogo dei Trigger @defer
| Trigger | Attivazione | Caso d'uso tipico |
|---|---|---|
on viewport |
L'elemento placeholder entra nel viewport | Contenuti below-the-fold, commenti, footer |
on idle |
Il browser è inattivo (requestIdleCallback) | Widget non critici, analytics |
on interaction |
Click o focus su un elemento specifico | Pannelli espandibili, dettagli on-demand |
on hover |
Mouse hover su un elemento specifico | Tooltip complessi, anteprime |
on timer(Ns) |
Dopo N secondi/millisecondi | Popup, contenuti ritardati |
when condizione |
Quando l'espressione diventa truthy | Contenuti basati su stato (login, flag) |
Prefetching Intelligente
@defer supporta anche il prefetching: puoi iniziare
a scaricare il chunk prima che il trigger di rendering si attivi. Questo migliora
l'esperienza utente perchè il contenuto è già pronto quando serve.
<!-- Prefetch on idle, render on viewport -->
<!-- Il browser scarica il chunk quando è inattivo, -->
<!-- ma lo rende visibile solo quando entra nel viewport -->
@defer (on viewport; prefetch on idle) {
<app-heavy-data-grid [data]="gridData()" />
} @placeholder {
<div class="grid-skeleton">
<div class="skeleton-row"></div>
<div class="skeleton-row"></div>
<div class="skeleton-row"></div>
</div>
}
<!-- Prefetch on hover, render on interaction -->
<!-- L'utente passa il mouse e il chunk si scarica, -->
<!-- quando clicca il contenuto appare istantaneamente -->
<button #editBtn>Modifica Profilo</button>
@defer (on interaction(editBtn); prefetch on hover(editBtn)) {
<app-profile-editor [user]="currentUser()" />
} @placeholder {
<p>Clicca per modificare il profilo</p>
}
@empty: Gestire Liste Vuote con Eleganza
Il blocco @empty è un compagno di @for che permette di
mostrare contenuto alternativo quando la collezione iterata è vuota. Prima della nuova
sintassi, questo richiedeva un *ngIf separato per controllare la lunghezza
dell'array.
<!-- PRIMA: due direttive separate -->
<div *ngIf="notifications.length === 0" class="empty-state">
<p>Nessuna notifica</p>
</div>
<div *ngFor="let notification of notifications">
{{ notification.message }}
</div>
<!-- DOPO: integrato nel control flow -->
@for (notification of notifications(); track notification.id) {
<div class="notification" [class]="notification.type">
<span class="icon">{{ notification.icon }}</span>
<p>{{ notification.message }}</p>
<time>{{ notification.date | date:'short' }}</time>
</div>
} @empty {
<div class="empty-state">
<img src="/assets/no-notifications.svg" alt="Nessuna notifica" />
<h3>Tutto in ordine!</h3>
<p>Non hai notifiche al momento.</p>
</div>
}
Vantaggi di @empty
- Elimina la duplicazione del check
array.length === 0 - Il blocco @empty viene renderizzato solo quando la collezione ha zero elementi
- Migliora la leggibilità raggruppando logica di iterazione e stato vuoto
- Funziona con qualsiasi iterabile: array, Signal di array, risultati filtrati
Migrazione Automatica con Schematics
Il team Angular fornisce uno schematic ufficiale che migra automaticamente i template dalla vecchia sintassi alla nuova. Questo strumento analizza ogni template, converte le direttive strutturali e genera codice pulito.
# Migrazione automatica del control flow
ng generate @angular/core:control-flow-migration
# Il comando:
# 1. Analizza tutti i template dell'applicazione
# 2. Converte *ngIf -> @if
# 3. Converte *ngFor -> @for (con track $index come default)
# 4. Converte ngSwitch -> @switch
# 5. Rimuove le importazioni di CommonModule dove non più necessarie
# Per un singolo componente
ng generate @angular/core:control-flow-migration --path=src/app/components/my-component
# Per una dry run (anteprima senza modificare i file)
ng generate @angular/core:control-flow-migration --dry-run
Attenzione alla Migrazione Automatica
- Track default: Lo schematic inserisce
track $indexcome default per i@for. Dopo la migrazione, rivedi ogni occorrenza e sostituisci con la proprietà univoca corretta (es.track item.id) - Formattazione: La migrazione potrebbe alterare l'indentazione. Esegui il formatter del progetto dopo la migrazione
- Template complessi: Template con direttive strutturali annidate e complesse potrebbero richiedere revisione manuale
- Test: Esegui tutti i test dopo la migrazione per verificare che il comportamento sia invariato
- Commit prima: Committa il codice prima di eseguire la migrazione, cosi puoi facilmente confrontare le differenze
<!-- PRIMA della migrazione -->
<div *ngIf="users$ | async as users; else loading">
<table>
<tr *ngFor="let user of users; trackBy: trackByUserId; let i = index">
<td>{{ i + 1 }}</td>
<td>{{ user.name }}</td>
<td [ngSwitch]="user.status">
<span *ngSwitchCase="'active'" class="green">Attivo</span>
<span *ngSwitchCase="'inactive'" class="red">Inattivo</span>
<span *ngSwitchDefault class="gray">N/D</span>
</td>
</tr>
</table>
</div>
<ng-template #loading>
<div class="spinner">Caricamento...</div>
</ng-template>
<!-- DOPO la migrazione automatica -->
@if (users$ | async; as users) {
<table>
@for (user of users; track user.id; let i = $index) {
<tr>
<td>{{ i + 1 }}</td>
<td>{{ user.name }}</td>
<td>
@switch (user.status) {
@case ('active') {
<span class="green">Attivo</span>
}
@case ('inactive') {
<span class="red">Inattivo</span>
}
@default {
<span class="gray">N/D</span>
}
}
</td>
</tr>
}
</table>
} @else {
<div class="spinner">Caricamento...</div>
}
Pattern Avanzati: Combinare il Control Flow
La vera potenza del nuovo control flow emerge quando si combinano i vari costrutti per risolvere scenari reali. Vediamo alcuni pattern avanzati utilizzati comunemente in produzione.
Pattern 1: Dashboard con Lazy Loading per Sezioni
<div class="dashboard">
<!-- Header sempre visibile -->
<header>
<h1>Dashboard</h1>
@if (user(); as u) {
<span>Benvenuto, {{ u.name }}</span>
}
</header>
<!-- KPI caricati immediatamente quando idle -->
@defer (on idle) {
<app-kpi-cards [metrics]="kpiData()" />
} @placeholder {
<div class="kpi-skeleton">
@for (i of [1,2,3,4]; track $index) {
<div class="skeleton-card"></div>
}
</div>
}
<!-- Grafico caricato quando entra nel viewport -->
@defer (on viewport; prefetch on idle) {
<app-revenue-chart [data]="revenueData()" />
} @placeholder {
<div class="chart-placeholder" style="height: 400px">
<p>Grafico ricavi</p>
</div>
} @loading (after 200ms) {
<div class="spinner">Caricamento grafico...</div>
}
<!-- Tabella dati caricata su richiesta -->
<button #showTable>Mostra dettagli</button>
@defer (on interaction(showTable); prefetch on hover(showTable)) {
@if (tableData(); as data) {
@for (row of data; track row.id) {
<app-data-row [row]="row" />
} @empty {
<p>Nessun dato disponibile per il periodo selezionato</p>
}
}
} @placeholder {
<p>Clicca per visualizzare i dettagli</p>
}
</div>
Pattern 2: Lista con Filtri, Ordinamento e Stato Vuoto
<div class="product-list">
<!-- Filtri -->
<div class="filters">
@for (category of categories(); track category.id) {
<button
[class.active]="selectedCategory() === category.id"
(click)="selectCategory(category.id)">
{{ category.name }}
<span class="count">({{ category.count }})</span>
</button>
}
</div>
<!-- Stato di caricamento -->
@if (isLoading()) {
<div class="loading">
@for (i of [1,2,3]; track $index) {
<div class="product-skeleton"></div>
}
</div>
} @else {
<!-- Lista prodotti -->
@for (product of filteredProducts(); track product.id) {
<div class="product-card">
<h3>{{ product.name }}</h3>
<p>{{ product.description }}</p>
@switch (product.availability) {
@case ('in_stock') {
<span class="badge success">Disponibile</span>
}
@case ('low_stock') {
<span class="badge warning">Ultime scorte ({{ product.stock }})</span>
}
@case ('out_of_stock') {
<span class="badge danger">Esaurito</span>
}
}
<div class="price">{{ product.price | currency:'EUR' }}</div>
</div>
} @empty {
<div class="empty-state">
<h3>Nessun prodotto trovato</h3>
<p>Prova a modificare i filtri di ricerca</p>
<button (click)="resetFilters()">Resetta filtri</button>
</div>
}
}
</div>
Pattern 3: @defer Condizionale con Permessi Utente
@Component({
selector: 'app-settings-page',
standalone: true,
template: `
<h1>Impostazioni</h1>
<!-- Sezione profilo: sempre disponibile -->
<app-profile-settings />
<!-- Pannello admin: caricato solo se l'utente è admin -->
@defer (when isAdmin()) {
<app-admin-settings />
} @placeholder {
@if (!isAdmin()) {
<div class="restricted">
<p>Accesso riservato agli amministratori</p>
</div>
}
}
<!-- Impostazioni avanzate: defer + condizione -->
@if (showAdvanced()) {
@defer (on idle) {
<app-advanced-settings />
} @loading {
<div class="spinner">Caricamento impostazioni avanzate...</div>
}
}
`
})
export class SettingsPageComponent {
private authService = inject(AuthService);
isAdmin = computed(() =>
this.authService.currentUser()?.role === 'admin'
);
showAdvanced = signal(false);
}
Impatto sulle Performance
Il nuovo control flow non è solo più leggibile: ha vantaggi concreti e misurabili sulle performance dell'applicazione. Il compilatore Angular genera codice ottimizzato che elimina l'overhead runtime delle direttive strutturali.
Benchmark: Nuovo Control Flow vs Direttive Strutturali
| Metrica | Direttive (*ngIf, *ngFor) | Nuovo Control Flow | Miglioramento |
|---|---|---|---|
| Bundle size (CommonModule) | ~12KB (minified) | 0KB (built-in compiler) | -100% |
| Rendering lista 1000 elementi | ~45ms | ~38ms | -15% |
| Re-rendering con modifica singola | ~8ms | ~3ms | -62% |
| Memory footprint per @for | Direttiva + template ref + embedded view | Solo istruzioni compilate | Significativo |
| Lazy loading con @defer | Non disponibile (solo route-level) | A livello di componente/template | Nuova funzionalità |
perchè il Nuovo Control Flow e Più Veloce
- Compilazione diretta: I blocchi
@ife@forvengono compilati in istruzioni JavaScript ottimizzate dal compilatore Angular, senza l'overhead di creare istanze di direttive a runtime - Nessuna ViewContainerRef: Le direttive strutturali usano
ViewContainerRefper creare/distruggere viste embedded. Il nuovo control flow gestisce il DOM direttamente - Track ottimizzato: L'algoritmo di diffing del
@forè stato riscritto da zero per essere più efficiente rispetto aDefaultIterableDiffer - Tree shaking: Non essendo direttive, non occupano spazio nel bundle se non usate. E anche se usate, il codice generato è minimale
- @defer chunk splitting: Il compilatore crea automaticamente chunk separati per il contenuto lazy, riducendo il payload iniziale
Best Practices per il Nuovo Control Flow
Dopo mesi di utilizzo in produzione dalla community Angular, si sono consolidate best practices chiare per sfruttare al meglio il nuovo control flow.
Le 10 Best Practices del Nuovo Control Flow
-
Usa sempre una proprietà univoca nel track: Evita
track $indexa meno che la lista non contenga valori primitivi senza identità. Preferiscitrack item.id -
Sfrutta @empty per stati vuoti: Ogni
@forche mostra dati all'utente dovrebbe avere un blocco@emptycon un messaggio utile -
Usa @defer per contenuti below-the-fold: Tutto ciò che non è visibile
al primo rendering dovrebbe essere wrappato in
@defer (on viewport) -
Combina prefetch e trigger: Usa
prefetch on idleper pre-scaricare chunk durante i momenti di inattivita del browser -
Preferisci @else if a @switch per pochi casi: Se hai 2-3 condizioni,
@if / @else ifè più leggibile di@switch - Fornisci sempre un @placeholder per @defer: L'utente deve capire che un contenuto sta per apparire; un placeholder vuoto confonde
-
Usa minimum e after in @loading: Evita il "flash" di loading
per contenuti che si caricano rapidamente con
@loading (after 150ms; minimum 300ms) - Non annidare troppo: Più di 3 livelli di control flow annidati rendono il template illeggibile. Estrai sotto-componenti
- Migra completamente: Non mischiare vecchia e nuova sintassi nello stesso template; confonde e complica la manutenzione
-
Rivedi dopo la migrazione automatica: Lo schematic usa
track $indexcome default. Rivedi ogni@fore assegna la track expression corretta
Anti-pattern da Evitare
<!-- ERRORE 1: Track per $index su liste dinamiche -->
<!-- Se un elemento viene inserito in testa, TUTTI i nodi DOM vengono ricreati -->
@for (user of users(); track $index) {
<app-user-card [user]="user" /> <!-- Ricreato inutilmente! -->
}
<!-- FIX: track user.id -->
<!-- ERRORE 2: @defer senza placeholder -->
<!-- L'utente vede uno spazio vuoto senza capire cosa succede -->
@defer (on viewport) {
<app-comments />
}
<!-- FIX: aggiungi @placeholder con indicazione visiva -->
<!-- ERRORE 3: @defer per contenuti critici above-the-fold -->
<!-- Il contenuto principale non deve essere lazy -->
@defer (on idle) {
<app-hero-section /> <!-- MAI! Il hero deve essere immediato -->
}
<!-- ERRORE 4: Annidamento eccessivo -->
@if (condition1()) {
@for (item of items(); track item.id) {
@if (item.visible) {
@switch (item.type) {
@case ('A') {
@for (sub of item.children; track sub.id) {
<!-- Troppo annidato! Estrai un componente -->
}
}
}
}
}
}
<!-- FIX: estrai sotto-componenti per ridurre l'annidamento -->
Checklist per la Migrazione
Checklist Migrazione al Nuovo Control Flow
- Committa il codice corrente prima della migrazione
- Esegui
ng generate @angular/core:control-flow-migration --dry-run - Rivedi i cambiamenti proposti
- Esegui la migrazione reale
- Sostituisci tutti i
track $indexcon proprietà univoche - Aggiungi blocchi
@emptydove appropriato - Valuta dove aggiungere
@deferper lazy loading - Rimuovi le importazioni di
CommonModule,NgIf,NgFor,NgSwitch - Esegui il formatter per uniformare l'indentazione
- Esegui tutti i test (unit + e2e)
- Verifica visivamente ogni pagina
Riepilogo e Prossimi Passi
Il nuovo control flow di Angular rappresenta un salto qualitativo nella developer experience
del framework. La sintassi con @if, @for, @switch
e @defer non è solo più leggibile: produce codice più performante, abilita
il lazy loading dichiarativo e forza best practices come il track obbligatorio.
Concetti Chiave di Questo Articolo
- @if / @else if / @else: Sostituisce
*ngIfcon supporto nativo per else-if e alias tramiteas - @for con track: Sostituisce
*ngForcon track obbligatorio per performance garantite. Include variabili implicite ($index, $first, $last, $even, $odd, $count) - @switch / @case / @default: Sostituisce
ngSwitchcon sintassi pulita e confronto stretto - @defer: Lazy loading dichiarativo con trigger (viewport, idle, interaction, hover, timer, when) e prefetching
- @empty: Gestione elegante delle liste vuote, integrata direttamente nel
@for - Migrazione: Lo schematic
@angular/core:control-flow-migrationautomatizza la conversione - Performance: Zero overhead di bundle (compilato, non direttive), diffing ottimizzato, chunk splitting con
@defer
Nel prossimo articolo della serie esploreremo gli Standalone Components, l'architettura che elimina completamente gli NgModule da Angular, semplificando la struttura dell'applicazione e migliorando il tree-shaking e il lazy loading a livello di route.
Domande frequenti
Qual è il motivo per cui le direttive strutturali non sono più utilizzate in Angular?
Le direttive strutturali presentano limiti strutturali che la nuova sintassi risolve completamente.
Come funziona l'alias con la keyword "as" nella nuova sintassi di Angular?
L'alias consente di assegnare un nome al risultato della condizione, utilissimo quando si lavora con Signal o Observable che restituiscono valori nullable.
Perché è obbligatorio specificare l'espressione track nella nuova sintassi di Angular?
L'espressione track fornisce ad Angular un'identità stabile per ogni elemento, permettendo aggiornamenti ottimali della lista e prevenendo problemi di performance.
Qual è il vantaggio principale del lazy loading dichiarativo introdotto con la nuova sintassi dei template?
Il lazy loading dichiarativo consente di caricare componenti, direttive e pipe on-demand direttamente dal template, senza necessità di codice TypeScript per il lazy loading.

Commenti
Caricamento commenti...
Accedi per commentare