<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Guide How-to Angular: Tailwind CSS, chart.js, Signals, ngrx e reactive forms avanzati

Guide How-To Angular: Tailwind CSS, Chart.js, Signals, NgRx e Reactive Forms avanzati

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 pending delle 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 touched o 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 track nei 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.

Conclusione

Angular offre strumenti potenti per creare applicazioni moderne, ma la differenza la fa il modo in cui vengono combinati. Tailwind CSS semplifica la UI, Chart.js permette di costruire dashboard efficaci, le animazioni migliorano l’esperienza, Signals rendono più semplice la reattività, NgRx struttura lo stato enterprise e Reactive Forms gestisce form complessi in modo robusto.

La scelta migliore non è usare sempre la tecnologia più avanzata, ma scegliere lo strumento più adatto al problema. Parti semplice, aggiungi complessità solo quando serve e mantieni il codice leggibile.

💬 Reader notes

0 notes

Write a note

Share your opinion, a suggestion or a compliment

Latest notes

No notes yet. Be the first to comment!