<link rel="stylesheet" href="/assets/fonts/inter/inter.css" />
All posts

Angular how-to guides: Tailwind CSS, Chart.js, Signals, NgRx and advanced Reactive Forms

Angular How-To Guides: Tailwind CSS, Chart.js, Signals, NgRx and Advanced Reactive Forms

Angular How-To Guides: practical integrations, state management and advanced Reactive Forms

Step-by-step guides are among the most useful content for developers because they solve real problems: installing a library, configuring a project correctly, choosing a state management strategy, building complex forms and preventing hard-to-find bugs.

In this article we will follow a practical path to work better with modern Angular: integrating Tailwind CSS, using Chart.js, animations, state management with Signals, NgRx and Akita, and building advanced reactive forms with synchronous and asynchronous validation.

1. Angular project prerequisites

Before integrating libraries or implementing complex architectures, it pays to start from a clean base. We will use a modern Angular project, preferably with standalone components, Reactive Forms and TypeScript.

Creating the project

npm install -g @angular/cli

ng new angular-how-to-guide
cd angular-how-to-guide

ng serve

If you are working on an existing project, make sure you have at least:

  • Angular updated to a recent version.
  • Node.js in a version compatible with Angular.
  • TypeScript configured correctly.
  • A clear structure for components, services, models and store.

Recommended structure

src/
  app/
    core/
      services/
      guards/
      interceptors/
    shared/
      components/
      pipes/
      validators/
    features/
      dashboard/
      products/
      profile/
    state/
      signals/
      ngrx/
    app.config.ts
    app.routes.ts

A tidy structure helps above all as the project grows. Separating core, shared, features and state helps avoid confusing dependencies and oversized components.

2. How to integrate Tailwind CSS in Angular

Tailwind CSS is a utility-first library that lets you build modern interfaces without writing large amounts of custom CSS. In Angular it is very useful for dashboards, admin panels, landing pages and reusable components.

Installation

npm install tailwindcss @tailwindcss/postcss postcss --save-dev

Create or update the PostCSS configuration file.

{
  "plugins": {
    "@tailwindcss/postcss": {}
  }
}

Then import Tailwind in the global stylesheet, for example src/styles.css.

@import "tailwindcss";

Example of an Angular component with 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 practices with Tailwind in Angular

  • Avoid huge templates full of repeated classes.
  • Extract reusable components when a piece of UI repeats.
  • Use conditional classes with [class.nome-classe] or [ngClass].
  • Define consistent design tokens for colours, spacing and typography.
  • Don't use Tailwind as an excuse to ignore accessibility.

Example with conditional classes

<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>

This technique is useful for tabs, active buttons, filters, selected items and validation states.

3. How to integrate Chart.js in Angular

Chart.js is a widely used library for line, bar, radar and doughnut charts and statistics dashboards. In Angular you can use it directly or through dedicated wrappers. Here we look at a direct, simple and controllable integration.

Installation

npm install chart.js

Creating the chart component

ng generate component features/dashboard/revenue-chart --standalone

Component TypeScript code

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();
  }
}

Why destroy the chart?

When an Angular component is removed from the page, Chart.js can leave listeners or objects in memory. Calling destroy() in ngOnDestroy() prevents memory leaks, especially in dashboards with many charts.

Updating data dynamically

updateChart(values: number[]): void {
  if (!this.chart) {
    return;
  }

  this.chart.data.datasets[0].data = values;
  this.chart.update();
}

In a real project, values often come from an HTTP service, a global store or a signal.

Practical tips

  • Use separate components for each complex chart.
  • Don't initialise Chart.js before the canvas is available.
  • Always destroy the chart in ngOnDestroy().
  • For SSR or Angular Universal, run Chart.js only in the browser.
  • Normalise the data before passing it to the chart.

4. How to add animations in Angular

Animations improve how an interface is perceived, but they should be used judiciously. The goal is not to "decorate" the page, but to help users understand what is happening: panels opening, notifications appearing, transitions between states and form feedback.

Installing the Angular Animations package

npm install @angular/animations

Configuration in app.config.ts

import { ApplicationConfig } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';

export const appConfig: ApplicationConfig = {
  providers: [
    provideAnimations()
  ]
};

Example of a fade + slide animation

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;
}

When to use an external library such as GSAP

Angular Animations is enough for many application interfaces. A library like GSAP becomes useful when you need complex timelines, coordinated animations, highly dynamic landing pages or sophisticated micro-interactions.

npm install gsap

Important rule: avoid animating heavy properties such as width, height or top when you can use transform and opacity, which are generally more efficient.

5. State management in Angular: Signals, NgRx and Akita

State management is one of the most important topics in Angular. At first a service with a few variables may seem enough, but as the app grows you need to choose a more structured approach.

Quick comparison

Solution When to use it Advantages Disadvantages
Signals Simple to medium local or shared state Modern syntax, simple reactivity, less boilerplate Requires architectural discipline in large projects
NgRx Enterprise applications, complex flows, auditability Predictable pattern, DevTools, effects, selectors More boilerplate and a learning curve
Akita Leaner store, entity store, teams that want less boilerplate Pragmatic API, convenient entity management A less central ecosystem than NgRx

Rule of thumb

  • Use Signals for local state, UI state and small to medium features.
  • Use NgRx when you have many asynchronous flows, roles, permissions, caching and advanced debugging.
  • Use Akita if you want a structured store that is less verbose than NgRx.
  • Don't introduce a global store if the problem can be solved with a simple service.

6. Hands-on tutorial: a store with Angular Signals

Signals let you represent reactive values in a simple way. They are particularly handy for creating small feature stores without introducing external libraries.

Let's create a 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);
    });
  }
}

Component using the 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 = '';
  }
}

Advantages of the Signals approach

  • The code is readable.
  • No actions, reducers or effects are needed for simple cases.
  • Values derived with computed() are very convenient.
  • State stays encapsulated in the service.
  • The store's methods are easy to test.

A limit to remember

Signals do not mean "automatic architecture". If the project grows a lot, you still need to define conventions: naming, service separation, persistence, caching, error handling and communication with APIs.

7. Hands-on tutorial: state management with NgRx

NgRx implements a pattern inspired by Redux. It is a good fit when you want predictable, traceable and easily debuggable state. It is widely used in enterprise applications.

Installation

ng add @ngrx/store
ng add @ngrx/effects
ng add @ngrx/store-devtools

Defining the model

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.'
              })
            );
          })
        );
      })
    );
  });
}

Store configuration

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
    })
  ]
};

Angular component with 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());
  }
}

When NgRx is the right choice

  • You have many features sharing data.
  • You have complex asynchronous flows.
  • You need DevTools and time-travel debugging.
  • The team must follow very clear patterns.
  • State must be predictable and centralised.

8. When to use Akita

Akita is a more pragmatic and less verbose state management solution than NgRx. It is useful when you want a structured store without writing lots of actions and reducers.

Installation

npm install @datorama/akita

Conceptual store example

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();
  }
}

Service to change the state

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();
  }
}

Practical choice

If your team already knows NgRx and works on enterprise applications, NgRx remains a very solid choice. If instead you want a store that is quicker to implement and less ceremonious, Akita can be a good alternative. For new, small to medium applications, consider Signals first.

9. Advanced Reactive Forms in Angular

Reactive Forms are one of Angular's most powerful tools. They let you build complex forms, validate input, manage dynamic states, create FormArrays, cross-field validation and asynchronous checks tied to APIs.

Practical scenario

Let's create a profile form with:

  • account details;
  • email with asynchronous validation;
  • password with a custom validator;
  • password confirmation;
  • a dynamic list of skills with FormArray;
  • clear error messages in the template.

Custom validators

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);
      })
    );
  };
}

Example API service

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)}`
    );
  }
}

Component with a complex Reactive Form

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());
  }
}

Form HTML template

<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>

Common mistakes with Reactive Forms

  • Putting too much logic in the template.
  • Not using markAllAsTouched() on submit.
  • Making asynchronous calls on every keystroke without updateOn: 'blur' or debounce.
  • Not separating validators into dedicated files.
  • Not handling the pending state of asynchronous validation.
  • Not typing forms correctly in large projects.

Best practices

  • Use Reactive Forms for complex and dynamic forms.
  • Use template-driven forms only for very simple forms.
  • Extract custom validators.
  • Show error messages only after touched or submit.
  • Disable submit while the form is pending.
  • Normalise the payload before sending it to the backend.

10. Testing stores and forms

A hands-on tutorial isn't complete without tests. There's no need to test every detail of the framework, but it is important to verify custom logic: stores, validators and data transformation.

Testing a password validator

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();
  });
});

Testing the Signals store

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 and maintenance

In Angular, performance and maintainability go hand in hand. A fast but chaotic app becomes hard to evolve. A well-organised app, on the other hand, also tends to be easier to optimise.

Practical tips

  • Split features into small components.
  • Use lazy loading for heavy pages and areas.
  • Avoid complex logic inside templates.
  • Use track in @for loops.
  • Destroy listeners, charts and subscriptions that aren't managed automatically.
  • Don't put everything in the global store.
  • Use computed values instead of recalculating data in the template.
  • Avoid overly aggressive asynchronous validation.

12. Final checklist for a solid Angular project

  • Does the project have a clear structure?
  • Are components small and focused?
  • Are external libraries encapsulated in components or services?
  • Is Chart.js destroyed correctly?
  • Is Tailwind used consistently?
  • Is state local when possible and global only when needed?
  • Do complex forms use Reactive Forms?
  • Are custom validators tested?
  • Are form errors understandable for the user?
  • Do asynchronous calls have error handling?
  • Is the code readable for a new developer too?

FAQ

Signals or NgRx: which is better?

It depends on complexity. Signals is great for simple or medium state. NgRx is better suited to large applications with complex flows, advanced debugging and large teams.

Can I use Signals and NgRx together?

Yes. You can use NgRx for global state and Signals for local UI state, filters, panels, tabs and derived values in components.

Is Tailwind CSS suitable for enterprise applications?

Yes, if used with clear conventions. It is important to create shared components and not duplicate classes everywhere without a plan.

Is Chart.js enough for complex dashboards?

For many dashboards, yes. If you need extremely advanced charts, custom interactions or large amounts of data, you might consider more specialised libraries.

When should I use Reactive Forms?

Use Reactive Forms when you have complex validation, dynamic forms, FormArrays, interdependent controls or asynchronous validation.

How should I handle form errors?

Show errors only after the field has been touched or after submit. Avoid showing too many errors while the user is still typing.

Does asynchronous validation slow the app down?

It can if it runs on every character. Use updateOn: 'blur', debounce and caching when needed.

Is Akita still a valid choice?

It can be in projects that already use it or in teams that prefer a lighter approach. For new projects, carefully consider Signals and NgRx before introducing an external library.

Is it better to use wrapper libraries for Chart.js?

It depends. A wrapper can simplify integration, but direct use gives more control. For simple cases, direct integration is often enough.

How do I avoid Angular components that are too large?

Extract logic into services, stores or validators. Split the UI into smaller components and use inputs/outputs or shared services when needed.

Conclusion

Angular offers powerful tools for building modern applications, but what makes the difference is how they are combined. Tailwind CSS simplifies the UI, Chart.js lets you build effective dashboards, animations improve the experience, Signals make reactivity simpler, NgRx structures enterprise state and Reactive Forms handle complex forms robustly.

The best choice is not always to use the most advanced technology, but to choose the tool best suited to the problem. Start simple, add complexity only when needed and keep the code readable.

💬 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!