Guide How-To Angular: integrazioni pratiche, gestione dello stato e Reactive Forms avanzati
Le guide passo-passo sono tra i contenuti più utili per gli sviluppatori perché risolvono problemi reali: installare una libreria, configurare correttamente un progetto, scegliere una strategia di gestione dello stato, creare form complessi e prevenire bug difficili da individuare.
In questo articolo vedremo un percorso pratico per lavorare meglio con Angular moderno: integrazione di Tailwind CSS, utilizzo di Chart.js, animazioni, gestione dello stato con Signals, NgRx e Akita, e costruzione di form reattivi avanzati con validazioni sincrone e asincrone.
1. Prerequisiti del progetto Angular
Prima di integrare librerie o implementare architetture complesse, conviene partire da una base pulita. Useremo un progetto Angular moderno, preferibilmente con componenti standalone, Reactive Forms e TypeScript.
Creazione del progetto
npm install -g @angular/cli
ng new angular-how-to-guide
cd angular-how-to-guide
ng serve
Se stai lavorando su un progetto già esistente, assicurati almeno di avere:
- Angular aggiornato a una versione recente.
- Node.js in una versione compatibile con Angular.
- TypeScript configurato correttamente.
- Una struttura chiara per componenti, servizi, modelli e store.
Struttura consigliata
src/
app/
core/
services/
guards/
interceptors/
shared/
components/
pipes/
validators/
features/
dashboard/
products/
profile/
state/
signals/
ngrx/
app.config.ts
app.routes.ts
Una struttura ordinata aiuta soprattutto quando il progetto cresce. Separare core, shared, features e state permette di evitare dipendenze confuse e componenti troppo grandi.
2. Come integrare Tailwind CSS in Angular
Tailwind CSS è una libreria utility-first che consente di creare interfacce moderne senza scrivere grandi quantità di CSS personalizzato. In Angular è molto utile per dashboard, pannelli admin, landing page e componenti riutilizzabili.
Installazione
npm install tailwindcss @tailwindcss/postcss postcss --save-dev
Crea o aggiorna il file di configurazione PostCSS.
{
"plugins": {
"@tailwindcss/postcss": {}
}
}
Poi importa Tailwind nel file globale degli stili, ad esempio src/styles.css.
@import "tailwindcss";
Esempio di componente Angular con Tailwind
import { Component } from '@angular/core';
@Component({
selector: 'app-pricing-card',
standalone: true,
template: `
<section class="mx-auto max-w-md rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<p class="text-sm font-medium uppercase tracking-wide text-indigo-600">Piano Pro</p>
<h2 class="mt-2 text-3xl font-bold text-slate-900">€29/mese</h2>
<p class="mt-3 text-slate-600">
Ideale per team che vogliono creare applicazioni Angular più velocemente.
</p>
<ul class="mt-6 space-y-2 text-sm text-slate-700">
<li>Componenti riutilizzabili</li>
<li>Dashboard responsive</li>
<li>Supporto a form complessi</li>
</ul>
<button class="mt-6 w-full rounded-xl bg-indigo-600 px-4 py-3 font-semibold text-white hover:bg-indigo-700">
Inizia ora
</button>
</section>
`
})
export class PricingCardComponent {}
Best practice con Tailwind in Angular
- Evita template enormi pieni di classi ripetute.
- Estrai componenti riutilizzabili quando una UI si ripete.
- Usa classi condizionali con
[class.nome-classe]o[ngClass]. - Definisci design token coerenti per colori, spaziature e tipografia.
- Non usare Tailwind come scusa per ignorare l’accessibilità.
Esempio con classi condizionali
<button
type="button"
class="rounded-lg px-4 py-2 font-semibold transition"
[class.bg-green-600]="isActive"
[class.text-white]="isActive"
[class.bg-slate-200]="!isActive"
[class.text-slate-700]="!isActive">
Stato elemento
</button>
Questa tecnica è utile per tab, pulsanti attivi, filtri, elementi selezionati e stati di validazione.
3. Come integrare Chart.js in Angular
Chart.js è una libreria molto usata per grafici lineari, a barre, radar, doughnut e dashboard statistiche. In Angular puoi usarla direttamente oppure tramite wrapper dedicati. Qui vediamo un’integrazione diretta, semplice e controllabile.
Installazione
npm install chart.js
Creazione del componente grafico
ng generate component features/dashboard/revenue-chart --standalone
Codice TypeScript del componente
import {
AfterViewInit,
Component,
ElementRef,
OnDestroy,
ViewChild
} from '@angular/core';
import {
Chart,
ChartConfiguration,
registerables
} from 'chart.js';
Chart.register(...registerables);
@Component({
selector: 'app-revenue-chart',
standalone: true,
template: `
<div class="h-80 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm">
<h2 class="mb-4 text-lg font-semibold text-slate-900">Ricavi mensili</h2>
<canvas #canvas></canvas>
</div>
`
})
export class RevenueChartComponent implements AfterViewInit, OnDestroy {
@ViewChild('canvas')
canvas!: ElementRef<HTMLCanvasElement>;
private chart?: Chart;
ngAfterViewInit(): void {
const config: ChartConfiguration = {
type: 'line',
data: {
labels: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu'],
datasets: [
{
label: 'Ricavi',
data: [12000, 15000, 14000, 21000, 26000, 31000],
borderColor: '#4f46e5',
backgroundColor: 'rgba(79, 70, 229, 0.15)',
tension: 0.35,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true
}
},
scales: {
y: {
beginAtZero: true
}
}
}
};
this.chart = new Chart(this.canvas.nativeElement, config);
}
ngOnDestroy(): void {
this.chart?.destroy();
}
}
Perché distruggere il grafico?
Quando un componente Angular viene rimosso dalla pagina, Chart.js può lasciare listener o oggetti in memoria.
Chiamare destroy() in ngOnDestroy() evita memory leak, soprattutto in dashboard con molti grafici.
Aggiornare i dati dinamicamente
updateChart(values: number[]): void {
if (!this.chart) {
return;
}
this.chart.data.datasets[0].data = values;
this.chart.update();
}
In un progetto reale, i valori arrivano spesso da un servizio HTTP, da uno store globale o da un signal.
Consigli pratici
- Usa componenti separati per ogni grafico complesso.
- Non inizializzare Chart.js prima che il canvas sia disponibile.
- Distruggi sempre il grafico in
ngOnDestroy(). - Per SSR o Angular Universal, esegui Chart.js solo lato browser.
- Normalizza i dati prima di passarli al grafico.
4. Come aggiungere animazioni in Angular
Le animazioni migliorano la percezione dell’interfaccia, ma vanno usate con criterio. L’obiettivo non è “decorare” la pagina, ma aiutare l’utente a capire cosa sta succedendo: apertura di pannelli, comparsa di notifiche, transizioni tra stati e feedback sui form.
Installazione del pacchetto Angular Animations
npm install @angular/animations
Configurazione in app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';
export const appConfig: ApplicationConfig = {
providers: [
provideAnimations()
]
};
Esempio di animazione fade + slide
import { Component } from '@angular/core';
import {
animate,
style,
transition,
trigger
} from '@angular/animations';
@Component({
selector: 'app-notification',
standalone: true,
animations: [
trigger('fadeSlide', [
transition(':enter', [
style({
opacity: 0,
transform: 'translateY(12px)'
}),
animate(
'180ms ease-out',
style({
opacity: 1,
transform: 'translateY(0)'
})
)
]),
transition(':leave', [
animate(
'140ms ease-in',
style({
opacity: 0,
transform: 'translateY(12px)'
})
)
])
])
],
template: `
@if (visible) {
<div
@fadeSlide
class="rounded-xl border border-green-200 bg-green-50 p-4 text-green-800">
Operazione completata con successo.
</div>
}
<button
class="mt-4 rounded-lg bg-green-600 px-4 py-2 text-white"
type="button"
(click)="visible = !visible">
Mostra/Nascondi
</button>
`
})
export class NotificationComponent {
visible = true;
}
Quando usare una libreria esterna come GSAP
Angular Animations è sufficiente per molte interfacce applicative. Una libreria come GSAP diventa utile quando servono timeline complesse, animazioni coordinate, landing page molto dinamiche o micro-interazioni sofisticate.
npm install gsap
Regola importante: evita di animare proprietà pesanti come width, height o top
quando puoi usare transform e opacity, che sono generalmente più efficienti.
5. Gestione dello stato in Angular: Signals, NgRx e Akita
La gestione dello stato è uno degli argomenti più importanti in Angular. All’inizio un servizio con qualche variabile può sembrare sufficiente, ma quando l’app cresce diventa necessario scegliere un approccio più strutturato.
Confronto rapido
| Soluzione | Quando usarla | Vantaggi | Svantaggi |
|---|---|---|---|
| Signals | Stato locale o condiviso semplice/medio | Sintassi moderna, reattività semplice, meno boilerplate | Richiede disciplina architetturale nei progetti grandi |
| NgRx | Applicazioni enterprise, flussi complessi, auditabilità | Pattern prevedibile, DevTools, effetti, selettori | Più boilerplate e curva di apprendimento |
| Akita | Store più snello, entity store, team che vogliono meno boilerplate | API pragmatica, gestione entità comoda | Ecosistema meno centrale rispetto a NgRx |
Regola pratica
- Usa Signals per stato locale, UI state e feature medio-piccole.
- Usa NgRx quando hai molti flussi asincroni, ruoli, permessi, caching e debugging avanzato.
- Usa Akita se vuoi uno store strutturato ma meno verboso rispetto a NgRx.
- Non introdurre uno store globale se il problema è risolvibile con un semplice servizio.
6. Tutorial pratico: store con Angular Signals
I Signals permettono di rappresentare valori reattivi in modo semplice. Sono particolarmente comodi per creare piccoli store di feature senza introdurre librerie esterne.
Creiamo un Todo Store
import { Injectable, computed, signal } from '@angular/core';
export interface Todo {
id: number;
title: string;
done: boolean;
}
export type TodoFilter = 'all' | 'active' | 'done';
@Injectable({
providedIn: 'root'
})
export class TodoStore {
private readonly todosState = signal<Todo[]>([]);
readonly filter = signal<TodoFilter>('all');
readonly todos = this.todosState.asReadonly();
readonly activeCount = computed(() => {
return this.todosState().filter(todo => !todo.done).length;
});
readonly completedCount = computed(() => {
return this.todosState().filter(todo => todo.done).length;
});
readonly visibleTodos = computed(() => {
const filter = this.filter();
const todos = this.todosState();
if (filter === 'active') {
return todos.filter(todo => !todo.done);
}
if (filter === 'done') {
return todos.filter(todo => todo.done);
}
return todos;
});
addTodo(title: string): void {
const cleanTitle = title.trim();
if (!cleanTitle) {
return;
}
const todo: Todo = {
id: Date.now(),
title: cleanTitle,
done: false
};
this.todosState.update(current => [todo, ...current]);
}
toggleTodo(id: number): void {
this.todosState.update(current => {
return current.map(todo => {
if (todo.id !== id) {
return todo;
}
return {
...todo,
done: !todo.done
};
});
});
}
removeTodo(id: number): void {
this.todosState.update(current => {
return current.filter(todo => todo.id !== id);
});
}
clearCompleted(): void {
this.todosState.update(current => {
return current.filter(todo => !todo.done);
});
}
}
Componente che usa lo store
import { Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { TodoStore } from './todo.store';
@Component({
selector: 'app-todos',
standalone: true,
imports: [FormsModule],
template: `
<section class="mx-auto max-w-2xl rounded-2xl bg-white p-6 shadow-sm">
<h2 class="text-2xl font-bold text-slate-900">Todo con Signals</h2>
<div class="mt-4 flex gap-2">
<input
class="flex-1 rounded-lg border border-slate-300 px-3 py-2"
placeholder="Nuova attività"
[(ngModel)]="title">
<button
type="button"
class="rounded-lg bg-indigo-600 px-4 py-2 text-white"
(click)="add()">
Aggiungi
</button>
</div>
<div class="mt-4 flex gap-2">
<button type="button" (click)="store.filter.set('all')">Tutti</button>
<button type="button" (click)="store.filter.set('active')">Attivi</button>
<button type="button" (click)="store.filter.set('done')">Completati</button>
</div>
<ul class="mt-6 space-y-2">
@for (todo of store.visibleTodos(); track todo.id) {
<li class="flex items-center justify-between rounded-lg border border-slate-200 p-3">
<label class="flex items-center gap-2">
<input
type="checkbox"
[checked]="todo.done"
(change)="store.toggleTodo(todo.id)">
<span [class.line-through]="todo.done">
{{ todo.title }}
</span>
</label>
<button
type="button"
class="text-red-600"
(click)="store.removeTodo(todo.id)">
Elimina
</button>
</li>
}
</ul>
<p class="mt-4 text-sm text-slate-600">
Attivi: {{ store.activeCount() }} -
Completati: {{ store.completedCount() }}
</p>
</section>
`
})
export class TodosComponent {
readonly store = inject(TodoStore);
title = '';
add(): void {
this.store.addTodo(this.title);
this.title = '';
}
}
Vantaggi dell’approccio con Signals
- Il codice è leggibile.
- Non servono action, reducer o effect per casi semplici.
- I valori derivati con
computed()sono molto comodi. - Lo stato resta incapsulato nel servizio.
- È facile testare i metodi dello store.
Limite da ricordare
Signals non significa “architettura automatica”. Se il progetto cresce molto, devi comunque definire convenzioni: naming, separazione dei servizi, persistenza, caching, gestione errori e comunicazione con API.
7. Tutorial pratico: gestione dello stato con NgRx
NgRx implementa un pattern ispirato a Redux. È indicato quando vuoi uno stato prevedibile, tracciabile e facilmente debuggabile. È molto usato in applicazioni enterprise.
Installazione
ng add @ngrx/store
ng add @ngrx/effects
ng add @ngrx/store-devtools
Definizione del modello
export interface Product {
id: number;
name: string;
price: number;
}
export interface ProductsState {
items: Product[];
loading: boolean;
error: string | null;
}
export const initialProductsState: ProductsState = {
items: [],
loading: false,
error: null
};
Actions
import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { Product } from './product.model';
export const ProductsActions = createActionGroup({
source: 'Products',
events: {
'Load Products': emptyProps(),
'Load Products Success': props<{ products: Product[] }>(),
'Load Products Failure': props<{ error: string }>()
}
});
Reducer
import { createReducer, on } from '@ngrx/store';
import { ProductsActions } from './products.actions';
import { initialProductsState } from './product.model';
export const productsReducer = createReducer(
initialProductsState,
on(ProductsActions.loadProducts, state => ({
...state,
loading: true,
error: null
})),
on(ProductsActions.loadProductsSuccess, (state, { products }) => ({
...state,
items: products,
loading: false
})),
on(ProductsActions.loadProductsFailure, (state, { error }) => ({
...state,
loading: false,
error
}))
);
Selectors
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ProductsState } from './product.model';
export const selectProductsState =
createFeatureSelector<ProductsState>('products');
export const selectProducts =
createSelector(
selectProductsState,
state => state.items
);
export const selectProductsLoading =
createSelector(
selectProductsState,
state => state.loading
);
export const selectProductsError =
createSelector(
selectProductsState,
state => state.error
);
Effects
import { Injectable, inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, of, switchMap } from 'rxjs';
import { ProductsActions } from './products.actions';
import { ProductsApiService } from './products-api.service';
@Injectable()
export class ProductsEffects {
private readonly actions$ = inject(Actions);
private readonly api = inject(ProductsApiService);
loadProducts$ = createEffect(() => {
return this.actions$.pipe(
ofType(ProductsActions.loadProducts),
switchMap(() => {
return this.api.getProducts().pipe(
map(products => ProductsActions.loadProductsSuccess({ products })),
catchError(() => {
return of(
ProductsActions.loadProductsFailure({
error: 'Errore durante il caricamento dei prodotti.'
})
);
})
);
})
);
});
}
Configurazione dello store
import { ApplicationConfig } from '@angular/core';
import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { productsReducer } from './state/products.reducer';
import { ProductsEffects } from './state/products.effects';
export const appConfig: ApplicationConfig = {
providers: [
provideStore({
products: productsReducer
}),
provideEffects([
ProductsEffects
]),
provideStoreDevtools({
maxAge: 25
})
]
};
Componente Angular con NgRx
import { Component, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { ProductsActions } from './state/products.actions';
import {
selectProducts,
selectProductsError,
selectProductsLoading
} from './state/products.selectors';
@Component({
selector: 'app-products-page',
standalone: true,
template: `
<section class="space-y-4">
<h2 class="text-2xl font-bold">Prodotti</h2>
@if (loading()) {
<p>Caricamento...</p>
}
@if (error()) {
<p class="text-red-600">{{ error() }}</p>
}
<ul class="grid gap-4 md:grid-cols-3">
@for (product of products(); track product.id) {
<li class="rounded-xl border border-slate-200 p-4">
<h3 class="font-semibold">{{ product.name }}</h3>
<p>€ {{ product.price }}</p>
</li>
}
</ul>
</section>
`
})
export class ProductsPageComponent {
private readonly store = inject(Store);
readonly products = this.store.selectSignal(selectProducts);
readonly loading = this.store.selectSignal(selectProductsLoading);
readonly error = this.store.selectSignal(selectProductsError);
constructor() {
this.store.dispatch(ProductsActions.loadProducts());
}
}
Quando NgRx è la scelta giusta
- Hai molte feature che condividono dati.
- Hai flussi asincroni complessi.
- Hai bisogno di DevTools e time-travel debugging.
- Il team deve seguire pattern molto chiari.
- Lo stato deve essere prevedibile e centralizzato.
8. Quando usare Akita
Akita è una soluzione di state management più pragmatica e meno verbosa rispetto a NgRx. È utile quando vuoi uno store strutturato, ma senza scrivere molte action e reducer.
Installazione
npm install @datorama/akita
Esempio concettuale di store
import { Injectable } from '@angular/core';
import {
EntityState,
EntityStore,
StoreConfig
} from '@datorama/akita';
export interface CartItem {
id: number;
name: string;
quantity: number;
}
export interface CartState extends EntityState<CartItem, number> {}
@Injectable({
providedIn: 'root'
})
@StoreConfig({
name: 'cart'
})
export class CartStore extends EntityStore<CartState> {
constructor() {
super();
}
}
Servizio per modificare lo stato
import { Injectable, inject } from '@angular/core';
import { CartStore, CartItem } from './cart.store';
@Injectable({
providedIn: 'root'
})
export class CartService {
private readonly cartStore = inject(CartStore);
addItem(item: CartItem): void {
this.cartStore.add(item);
}
updateQuantity(id: number, quantity: number): void {
this.cartStore.update(id, {
quantity
});
}
removeItem(id: number): void {
this.cartStore.remove(id);
}
clearCart(): void {
this.cartStore.reset();
}
}
Scelta pratica
Se il tuo team conosce già NgRx e lavora su applicazioni enterprise, NgRx resta una scelta molto solida. Se invece vuoi uno store più rapido da implementare e meno cerimonioso, Akita può essere una buona alternativa. Per applicazioni nuove e medio-piccole, valuta prima Signals.
9. Reactive Forms avanzati in Angular
I Reactive Forms sono uno degli strumenti più potenti di Angular. Permettono di costruire form complessi, validare input, gestire stati dinamici, creare FormArray, validazioni cross-field e controlli asincroni collegati ad API.
Scenario pratico
Creiamo un form profilo con:
- dati account;
- email con validazione asincrona;
- password con validatore personalizzato;
- conferma password;
- lista dinamica di competenze con
FormArray; - messaggi di errore chiari nel template.
Validatori personalizzati
import {
AbstractControl,
AsyncValidatorFn,
ValidationErrors,
ValidatorFn
} from '@angular/forms';
import {
catchError,
map,
of,
switchMap,
timer
} from 'rxjs';
import { UsersApiService } from './users-api.service';
export function passwordStrengthValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = String(control.value || '');
const hasMinLength = value.length >= 8;
const hasUppercase = /[A-Z]/.test(value);
const hasNumber = /[0-9]/.test(value);
const hasSpecial = /[^A-Za-z0-9]/.test(value);
const valid =
hasMinLength &&
hasUppercase &&
hasNumber &&
hasSpecial;
if (valid) {
return null;
}
return {
weakPassword: {
hasMinLength,
hasUppercase,
hasNumber,
hasSpecial
}
};
};
}
export function passwordsMatchValidator(
passwordKey: string,
confirmPasswordKey: string
): ValidatorFn {
return (group: AbstractControl): ValidationErrors | null => {
const password = group.get(passwordKey)?.value;
const confirmPassword = group.get(confirmPasswordKey)?.value;
if (password === confirmPassword) {
return null;
}
return {
passwordsMismatch: true
};
};
}
export function uniqueEmailValidator(
usersApi: UsersApiService
): AsyncValidatorFn {
return (control: AbstractControl) => {
const email = String(control.value || '').trim();
if (!email) {
return of(null);
}
return timer(400).pipe(
switchMap(() => usersApi.emailExists(email)),
map(exists => {
return exists ? { emailTaken: true } : null;
}),
catchError(() => {
return of(null);
})
);
};
}
Servizio API di esempio
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class UsersApiService {
private readonly http = inject(HttpClient);
emailExists(email: string): Observable<boolean> {
return this.http.get<boolean>(
`/api/users/email-exists?email=${encodeURIComponent(email)}`
);
}
}
Componente con Reactive Form complesso
import { Component, inject } from '@angular/core';
import {
FormArray,
NonNullableFormBuilder,
ReactiveFormsModule,
Validators
} from '@angular/forms';
import {
passwordStrengthValidator,
passwordsMatchValidator,
uniqueEmailValidator
} from './profile.validators';
import { UsersApiService } from './users-api.service';
@Component({
selector: 'app-profile-form',
standalone: true,
imports: [
ReactiveFormsModule
],
templateUrl: './profile-form.component.html'
})
export class ProfileFormComponent {
private readonly fb = inject(NonNullableFormBuilder);
private readonly usersApi = inject(UsersApiService);
readonly form = this.fb.group({
account: this.fb.group(
{
name: [
'',
[
Validators.required,
Validators.minLength(2)
]
],
email: [
'',
{
validators: [
Validators.required,
Validators.email
],
asyncValidators: [
uniqueEmailValidator(this.usersApi)
],
updateOn: 'blur'
}
],
password: [
'',
[
Validators.required,
passwordStrengthValidator()
]
],
confirmPassword: [
'',
[
Validators.required
]
]
},
{
validators: [
passwordsMatchValidator('password', 'confirmPassword')
]
}
),
profile: this.fb.group({
role: ['frontend'],
bio: ['']
}),
skills: this.fb.array([
this.createSkill()
])
});
get skills(): FormArray {
return this.form.controls.skills;
}
createSkill() {
return this.fb.group({
name: [
'',
[
Validators.required
]
],
years: [
0,
[
Validators.required,
Validators.min(0),
Validators.max(40)
]
]
});
}
addSkill(): void {
this.skills.push(this.createSkill());
}
removeSkill(index: number): void {
if (this.skills.length === 1) {
return;
}
this.skills.removeAt(index);
}
submit(): void {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
console.log('Payload form:', this.form.getRawValue());
}
}
Template HTML del form
<form
class="mx-auto max-w-3xl space-y-8 rounded-2xl bg-white p-6 shadow-sm"
[formGroup]="form"
(ngSubmit)="submit()">
<section formGroupName="account" class="space-y-4">
<h2 class="text-xl font-bold text-slate-900">Account</h2>
<div>
<label class="block text-sm font-medium text-slate-700">
Nome
</label>
<input
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
type="text"
formControlName="name">
@if (form.controls.account.controls.name.touched &&
form.controls.account.controls.name.hasError('required')) {
<p class="mt-1 text-sm text-red-600">
Il nome è obbligatorio.
</p>
}
</div>
<div>
<label class="block text-sm font-medium text-slate-700">
Email
</label>
<input
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
type="email"
formControlName="email">
@if (form.controls.account.controls.email.pending) {
<p class="mt-1 text-sm text-slate-500">
Verifica email in corso...
</p>
}
@if (form.controls.account.controls.email.hasError('emailTaken')) {
<p class="mt-1 text-sm text-red-600">
Questa email è già registrata.
</p>
}
</div>
<div>
<label class="block text-sm font-medium text-slate-700">
Password
</label>
<input
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
type="password"
formControlName="password">
@if (form.controls.account.controls.password.hasError('weakPassword')) {
<p class="mt-1 text-sm text-red-600">
La password deve avere almeno 8 caratteri, una maiuscola,
un numero e un carattere speciale.
</p>
}
</div>
<div>
<label class="block text-sm font-medium text-slate-700">
Conferma password
</label>
<input
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
type="password"
formControlName="confirmPassword">
@if (form.controls.account.hasError('passwordsMismatch')) {
<p class="mt-1 text-sm text-red-600">
Le password non coincidono.
</p>
}
</div>
</section>
<section formGroupName="profile" class="space-y-4">
<h2 class="text-xl font-bold text-slate-900">Profilo</h2>
<div>
<label class="block text-sm font-medium text-slate-700">
Ruolo
</label>
<select
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
formControlName="role">
<option value="frontend">Frontend Developer</option>
<option value="backend">Backend Developer</option>
<option value="fullstack">Full Stack Developer</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">
Bio
</label>
<textarea
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
rows="4"
formControlName="bio"></textarea>
</div>
</section>
<section class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold text-slate-900">
Competenze
</h2>
<button
type="button"
class="rounded-lg bg-indigo-600 px-4 py-2 text-white"
(click)="addSkill()">
Aggiungi competenza
</button>
</div>
<div formArrayName="skills" class="space-y-3">
@for (skill of skills.controls; track $index) {
<div
class="grid gap-3 rounded-xl border border-slate-200 p-4 md:grid-cols-[1fr_140px_auto]"
[formGroupName]="$index">
<input
class="rounded-lg border border-slate-300 px-3 py-2"
type="text"
placeholder="Competenza"
formControlName="name">
<input
class="rounded-lg border border-slate-300 px-3 py-2"
type="number"
placeholder="Anni"
formControlName="years">
<button
type="button"
class="rounded-lg border border-red-300 px-4 py-2 text-red-600"
(click)="removeSkill($index)">
Rimuovi
</button>
</div>
}
</div>
</section>
<button
type="submit"
class="w-full rounded-xl bg-slate-900 px-5 py-3 font-semibold text-white disabled:opacity-50"
[disabled]="form.pending">
Salva profilo
</button>
</form>
Errori comuni nei Reactive Forms
- Mettere troppa logica nel template.
- Non usare
markAllAsTouched()al submit. - Fare chiamate asincrone a ogni tasto premuto senza
updateOn: 'blur'o debounce. - Non separare i validatori in file dedicati.
- Non gestire lo stato
pendingdelle validazioni asincrone. - Non tipizzare correttamente i form nei progetti grandi.
Best practice
- Usa Reactive Forms per form complessi e dinamici.
- Usa Template-driven forms solo per form molto semplici.
- Estrai i validatori personalizzati.
- Mostra messaggi di errore solo dopo
touchedo submit. - Disabilita il submit mentre il form è
pending. - Normalizza il payload prima di inviarlo al backend.
10. Testare store e form
Un tutorial pratico non è completo senza test. Non serve testare ogni dettaglio del framework, ma è importante verificare la logica personalizzata: store, validatori e trasformazione dei dati.
Test di un validatore password
import { FormControl } from '@angular/forms';
import { passwordStrengthValidator } from './profile.validators';
describe('passwordStrengthValidator', () => {
it('dovrebbe invalidare una password debole', () => {
const control = new FormControl('abc');
const validator = passwordStrengthValidator();
const result = validator(control);
expect(result).toEqual(
jasmine.objectContaining({
weakPassword: jasmine.any(Object)
})
);
});
it('dovrebbe accettare una password forte', () => {
const control = new FormControl('Angular2026!');
const validator = passwordStrengthValidator();
const result = validator(control);
expect(result).toBeNull();
});
});
Test dello store con Signals
import { TodoStore } from './todo.store';
describe('TodoStore', () => {
it('dovrebbe aggiungere una todo', () => {
const store = new TodoStore();
store.addTodo('Studiare Angular Signals');
expect(store.todos().length).toBe(1);
expect(store.todos()[0].title).toBe('Studiare Angular Signals');
});
it('dovrebbe filtrare le todo completate', () => {
const store = new TodoStore();
store.addTodo('Prima attività');
const id = store.todos()[0].id;
store.toggleTodo(id);
store.filter.set('done');
expect(store.visibleTodos().length).toBe(1);
expect(store.visibleTodos()[0].done).toBeTrue();
});
});
11. Performance e manutenzione
In Angular, performance e manutenzione sono collegate. Un’app veloce ma caotica diventa difficile da evolvere. Un’app ben organizzata, invece, tende anche a essere più semplice da ottimizzare.
Consigli pratici
- Dividi le feature in componenti piccoli.
- Usa lazy loading per pagine e aree pesanti.
- Evita logica complessa dentro i template.
- Usa
tracknei cicli@for. - Distruggi listener, grafici e subscription non gestite automaticamente.
- Non mettere tutto nello store globale.
- Usa computed values invece di ricalcolare dati nel template.
- Evita validazioni asincrone troppo aggressive.
12. Checklist finale per un progetto Angular solido
- Il progetto ha una struttura chiara?
- I componenti sono piccoli e focalizzati?
- Le librerie esterne sono incapsulate in componenti o servizi?
- Chart.js viene distrutto correttamente?
- Tailwind è usato con coerenza?
- Lo stato è locale quando possibile e globale solo quando serve?
- I form complessi usano Reactive Forms?
- I validatori personalizzati sono testati?
- Gli errori del form sono comprensibili per l’utente?
- Le chiamate asincrone hanno gestione errori?
- Il codice è leggibile anche per un nuovo sviluppatore?
FAQ
Meglio Signals o NgRx?
Dipende dalla complessità. Signals è ottimo per stato semplice o medio. NgRx è più adatto a grandi applicazioni con flussi complessi, debugging avanzato e team numerosi.
Posso usare Signals e NgRx insieme?
Sì. Puoi usare NgRx per lo stato globale e Signals per UI state locale, filtri, pannelli, tab e valori derivati nei componenti.
Tailwind CSS è adatto ad applicazioni enterprise?
Sì, se usato con convenzioni chiare. È importante creare componenti condivisi e non duplicare classi ovunque senza criterio.
Chart.js è sufficiente per dashboard complesse?
Per molte dashboard sì. Se servono grafici estremamente avanzati, interazioni personalizzate o grandi quantità di dati, potresti valutare librerie più specializzate.
Quando usare Reactive Forms?
Usa Reactive Forms quando hai validazioni complesse, form dinamici, FormArray, controlli dipendenti tra loro o validazioni asincrone.
Come gestire gli errori nei form?
Mostra errori solo dopo che il campo è stato toccato oppure dopo il submit. Evita di mostrare troppi errori mentre l’utente sta ancora digitando.
Le validazioni asincrone rallentano l’app?
Possono rallentarla se eseguite a ogni carattere.
Usa updateOn: 'blur', debounce e caching quando necessario.
Akita è ancora una scelta valida?
Può esserlo in progetti che lo usano già o in team che preferiscono un approccio più leggero. Per nuovi progetti, valuta attentamente Signals e NgRx prima di introdurre una libreria esterna.
È meglio usare librerie wrapper per Chart.js?
Dipende. Un wrapper può semplificare l’integrazione, ma l’uso diretto offre più controllo. Per casi semplici, l’integrazione diretta è spesso sufficiente.
Come evitare componenti Angular troppo grandi?
Estrai logica in servizi, store o validator. Dividi la UI in componenti più piccoli e usa input/output o servizi condivisi quando serve.