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

Guides pratiques angulaires : Tailwind CSS, chart.js, Signals, ngrx et advanced reactive forms

Guides pratiques angulaires : Tailwind CSS, Chart.js, Signals, NgRx et Advanced Reactive Forms

Guides pratiques Angular : intégrations pratiques, gestion des états et formulaires réactifs avancés

Les guides étape par étape font partie des contenus les plus utiles pour les développeurs car ils résolvent de vrais problèmes : installer une bibliothèque, configurer correctement un projet, choisir une stratégie de gestion des états, créez des formulaires complexes et évitez les bugs difficiles à détecter.

Dans cet article, nous verrons un chemin pratique pour mieux travailler avec Angular moderne : Intégration CSS Tailwind, utilisation de Chart.js, animations, gestion des états avec Signals, NgRx et Akita, et créer des formulaires réactifs avancés avec des validations synchrones et asynchrones.

1. Conditions préalables du projet Angular

Avant d’intégrer des bibliothèques ou de mettre en œuvre des architectures complexes, il est préférable de repartir de zéro. Nous utiliserons un projet Angular moderne, de préférence avec des composants autonomes, Reactive Forms et TypeScript.

Création du projet

npm install -g @angular/cli

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

ng serve

Si vous travaillez sur un projet existant, assurez-vous au moins d'avoir :

  • Angular mis à jour vers une version récente.
  • Node.js dans une version compatible Angular.
  • TypeScript configuré correctement.
  • Une structure claire pour les composants, les services, les modèles et les magasins.

Structure recommandée

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

Une structure ordonnée est particulièrement utile lorsque le projet se développe. Séparer les fonctionnalités principales, partagées et l'état vous permet d'éviter les dépendances confuses et les composants trop volumineux.

2. Comment intégrer Tailwind CSS dans Angular

Tailwind CSS est une bibliothèque utilitaire qui vous permet de créer des interfaces modernes sans écrire de grandes quantités de CSS personnalisé. Dans Angular, il est très utile pour les tableaux de bord, les panneaux d'administration, les pages de destination et les composants réutilisables.

Installation

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

Créez ou mettez à jour le fichier de configuration PostCSS.

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

Importez ensuite Tailwind dans le fichier de styles globaux, par exemple src/styles.css.

@import "tailwindcss";

Exemple de composant angulaire avec 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 {}

Bonnes pratiques avec Tailwind dans Angular

  • Évitez les énormes modèles remplis de cours répétitifs.
  • Extraire les composants réutilisables lors de la répétition d'une interface utilisateur.
  • Utilisez des classes conditionnelles avec [class.class-name] ou [ngClass].
  • Définissez des conceptions de jetons cohérentes pour les couleurs, l'espacement et la typographie.
  • N'utilisez pas Tailwind comme excuse pour ignorer l'accessibilité.

Exemple avec classes conditionnelles

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

Cette technique est utile pour les onglets, les boutons actifs, les filtres, les éléments sélectionnés et les états de validation.

3. Comment intégrer Chart.js dans Angular

Chart.js est une bibliothèque largement utilisée pour les graphiques de tableaux de bord linéaires, à barres, radar, en anneau et statistiques. Dans Angular, vous pouvez l'utiliser directement ou via des wrappers dédiés. Nous voyons ici une intégration directe, simple et contrôlable.

Installation

npm install chart.js

Création du composant graphique

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

Code TypeScript du composant

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

Pourquoi détruire le graphique ?

Lorsqu'un composant angulaire est supprimé de la page, Chart.js peut laisser des écouteurs ou des objets en mémoire. L'appel de destroy() dans ngOnDestroy() évite les fuites de mémoire, notamment dans les tableaux de bord comportant de nombreux graphiques.

Mettre à jour les données de manière dynamique

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

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

Dans un projet réel, les valeurs proviennent souvent d'un service HTTP, d'un magasin global ou d'un signal.

Conseils pratiques

  • Utilisez des composants distincts pour chaque graphique complexe.
  • Ne pas initialiser Chart.js avant que le canevas ne soit disponible.
  • Toujours détruire le graphique dans ngOnDestroy().
  • Pour SSR ou Angular Universal, exécutez Chart.js uniquement côté navigateur.
  • Normalisez les données avant de les transmettre au graphique.

4. Comment ajouter des animations dans Angular

Les animations améliorent la perception de l'interface, mais doivent être utilisées à bon escient. Le but n’est pas de « décorer » la page, mais d’aider l’utilisateur à comprendre ce qui se passe : ouverture des panneaux, apparition des notifications, transitions entre les états et commentaires sur les formulaires.

Installation du package Angular Animations

npm install @angular/animations

Configuration dans app.config.ts

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

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

Exemple d'animation fondu + diapositive

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

Quand utiliser une bibliothèque externe comme GSAP

Les animations angulaires sont suffisantes pour de nombreuses interfaces d'application. Une bibliothèque comme GSAP devient utile lorsque vous avez besoin de chronologies complexes, d'animations coordonnées, des pages de destination très dynamiques ou des micro-interactions sophistiquées.

npm install gsap

Règle importante : évitez d'animer des propriétés lourdes telles que width, height ou top. quand vous pouvez utiliser transform et opacity, qui sont généralement plus efficaces.

5. Gestion de l'état en Angular : Signals, NgRx et Akita

La gestion de l'état est l'un des sujets les plus importants d'Angular. Au début, un service avec quelques variables peut sembler suffisant, mais à mesure que l'application se développe, cela devient nécessaire choisissez une approche plus structurée.

Comparaison rapide

Solution Quand l'utiliser Avantages Inconvénients
Signaux Etat simple/moyen local ou partagé Syntaxe moderne, réactivité simple, moins passe-partout Nécessite une discipline architecturale dans les grands projets
NgRx Applications d'entreprise, flux complexes, auditabilité Modèles prévisibles, outils de développement, effets, sélecteurs Plus de passe-partout et de courbe d'apprentissage
Akita Magasin plus allégé, magasin d'entité, équipes voulant moins de passe-partout API pragmatique, gestion pratique des entités Écosystème moins central que NgRx

Règle générale

  • Utilisez les signaux pour l'état local, l'état de l'interface utilisateur et les fonctionnalités petites à moyennes.
  • Utilisez NgRx lorsque vous disposez de nombreux flux, rôles, autorisations, mises en cache et débogage avancés asynchrones.
  • Utilisez Akita si vous souhaitez un magasin structuré mais moins verbeux que NgRx.
  • N'introduisez pas de magasin mondial si le problème peut être résolu avec un simple service.

6. Tutoriel pratique : stocker avec Angular Signals

Les signaux vous permettent de représenter des valeurs réactives de manière simple. Ils sont particulièrement pratiques pour créer de petits magasins de fonctionnalités sans introduire de bibliothèques externes.

Créons un magasin Todo

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

Composant qui utilise le magasin

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

Avantages de l'approche Signaux

  • Le code est lisible.
  • Pas besoin d'action, de réducteur ou d'effet pour les cas simples.
  • Les valeurs dérivées avec computed() sont très pratiques.
  • L'État reste encapsulé dans le service.
  • Il est facile de tester les méthodes du magasin.

Limite à retenir

Signaux ne signifie pas « architecture automatique ». Si le projet grandit beaucoup, il faut quand même définir des conventions : dénomination, séparation des services, persistance, mise en cache, gestion des erreurs et communication avec les API.

7. Tutoriel pratique : Gestion d'état avec NgRx

NgRx implémente un modèle inspiré de Redux. Il convient lorsque vous souhaitez un état prévisible, traçable et facilement débogué. Il est largement utilisé dans les applications d’entreprise.

Installation

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

Définition du modèle

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

Réducteur

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

Sélecteurs

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

Effets

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

Configuration du magasin

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

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

Quand NgRx est le bon choix

  • Vous disposez de nombreuses fonctionnalités qui partagent des données.
  • Vous disposez de flux asynchrones complexes.
  • Vous avez besoin de DevTools et du débogage du voyage dans le temps.
  • L'équipe doit suivre des schémas très clairs.
  • L'État doit être prévisible et centralisé.

8. Quand utiliser Akita

Akita est une solution de gestion d'état plus pragmatique et moins verbeuse que NgRx. C'est utile lorsque vous souhaitez un magasin structuré, mais sans écrire beaucoup d'actions et de réducteurs.

Installation

npm install @datorama/akita

Exemple conceptuel de magasin

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 pour changer le statut

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

Choix pratique

Si votre équipe connaît déjà NgRx et travaille sur des applications d'entreprise, NgRx reste un choix très solide. Si toutefois vous souhaitez un magasin plus rapide à mettre en place et moins cérémonieux, Akita peut être une bonne alternative. Pour les applications nouvelles et petites à moyennes, pensez d’abord aux signaux.

9. Formulaires réactifs avancés dans Angular

Les formulaires réactifs sont l'un des outils les plus puissants d'Angular. Ils permettent de construire des formulaires complexes, de valider des entrées, de gérer des états dynamiques, de créer des FormArrays, validations cross-field et contrôles asynchrones liés aux API.

Scénario pratique

Créons un formulaire de profil avec :

  • données du compte ;
  • email avec validation asynchrone ;
  • mot de passe avec validateur personnalisé ;
  • confirmer le mot de passe ;
  • liste dynamique de compétences avec FormArray;
  • effacer les messages d'erreur dans le modèle.

Validateurs personnalisés

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

Exemple de service API

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

Composant à forme réactive complexe

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

Modèle de formulaire HTML

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

Erreurs courantes dans les formulaires réactifs

  • Mettre trop de logique dans le modèle.
  • Ne pas utiliser markAllAsTouched() lors de la soumission.
  • Effectuez des appels asynchrones à chaque pression de touche sans updateOn : 'flou' ou anti-rebond.
  • Ne séparez pas les validateurs en fichiers dédiés.
  • Ne pas gérer le statut en attente des validations asynchrones.
  • Ne saisissez pas correctement les formulaires dans les grands projets.

Bonnes pratiques

  • Utilisez des formulaires réactifs pour les formulaires complexes et dynamiques.
  • Utilisez des formulaires basés sur un modèle uniquement pour les formulaires très simples.
  • Extraire les validateurs personnalisés.
  • Afficher les messages d'erreur uniquement après avoir touché ou soumis.
  • Désactiver la soumission lorsque le formulaire est en attente.
  • Normalisez la charge utile avant de l'envoyer au backend.

10. Testez le magasin et le formulaire

Un didacticiel pratique n’est pas complet sans tests. Vous n'avez pas besoin de tester chaque détail du framework, mais il est important de tester votre logique personnalisée : magasins, validateurs et transformation des données.

Test d'un validateur de mot de passe

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

Tests de magasin avec signaux

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. Performances et maintenance

Dans Angular, performances et maintenance sont liées. Une application rapide mais chaotique devient difficile à faire évoluer. En revanche, une application bien organisée a également tendance à être plus facile à optimiser.

Conseils pratiques

  • Diviser les fonctionnalités en petits composants.
  • Utilisez le chargement paresseux pour les pages et les zones lourdes.
  • Évitez la logique complexe à l'intérieur des modèles.
  • Utilisez track en boucles @for.
  • Détruisez les auditeurs, les graphiques et les abonnements non gérés automatiquement.
  • Ne mettez pas tout dans le magasin global.
  • Utilisez les valeurs calculées au lieu de recalculer les données dans le modèle.
  • Évitez les validations asynchrones trop agressives.

12. Liste de contrôle finale pour un projet angulaire solide

  • Le projet a-t-il une structure claire ?
  • Les composants sont-ils petits et ciblés ?
  • Les bibliothèques externes sont-elles encapsulées dans des composants ou des services ?
  • Chart.js est-il détruit correctement ?
  • Tailwind est-il utilisé de manière cohérente ?
  • L'État est-il local lorsque cela est possible et global seulement lorsque cela est nécessaire ?
  • Les formulaires complexes utilisent-ils des formulaires réactifs ?
  • Les validateurs personnalisés sont-ils testés ?
  • Les erreurs du formulaire sont-elles compréhensibles par l'utilisateur ?
  • Les appels asynchrones comportent-ils une gestion des erreurs ?
  • Le code est-il lisible même pour un nouveau développeur ?

FAQ

Meilleurs signaux ou NgRx ?

Cela dépend de la complexité. Les signaux sont parfaits pour les statuts simples ou moyens. NgRx est mieux adapté aux grandes applications avec des flux complexes, un débogage avancé et de grandes équipes.

Puis-je utiliser Signals et NgRx ensemble ?

Oui. Vous pouvez utiliser NgRx pour l'état global et les signaux pour l'état de l'interface utilisateur locale, les filtres, les panneaux, onglet et valeurs dérivées dans les composants.

Tailwind CSS est-il adapté aux applications d'entreprise ?

Oui, si utilisé avec des conventions claires. Il est important de créer des composants partagés et de ne pas dupliquer les classes partout sans critères.

Chart.js est-il suffisant pour les tableaux de bord complexes ?

Pour de nombreux tableaux de bord, oui. Si vous avez besoin de graphiques extrêmement avancés, d'interactions personnalisées ou de grandes quantités de données, vous pourriez envisager des bibliothèques plus spécialisées.

Quand utiliser les formulaires réactifs ?

Utilisez des formulaires réactifs lorsque vous avez des validations complexes, des formulaires dynamiques, des FormArrays, contrôles mutuellement dépendants ou validations asynchrones.

Comment gérer les erreurs dans les formulaires ?

Afficher les erreurs uniquement après avoir touché le champ ou après la soumission. Évitez d'afficher trop d'erreurs pendant que l'utilisateur est encore en train de taper.

Les validations asynchrones ralentissent-elles l'application ?

Ils peuvent le ralentir si chaque personnage le fait. Utilisez updateOn: 'blur', anti-rebond et mise en cache si nécessaire.

Akita est-il toujours un bon choix ?

Cela peut être dans des projets qui l'utilisent déjà ou dans des équipes qui préfèrent une approche plus légère. Pour les nouveaux projets, évaluez soigneusement Signals et NgRx avant d’introduire une bibliothèque externe.

Est-il préférable d'utiliser des bibliothèques wrapper pour Chart.js ?

Cela dépend. Un wrapper peut simplifier l’intégration, mais son utilisation directe offre plus de contrôle. Pour les cas simples, une intégration directe est souvent suffisante.

Comment éviter les composants angulaires trop grands ?

Extrayez la logique dans les services, les magasins ou les validateurs. Divisez l'interface utilisateur en composants plus petits et utilisez les entrées/sorties ou les services partagés si nécessaire.

Conclusion

Angular offre des outils puissants pour créer des applications modernes, mais la différence réside dans la manière dont ils sont combinés. Tailwind CSS simplifie l'interface utilisateur, Chart.js vous permet de créer des tableaux de bord efficaces, les animations améliorent l'expérience, Les signaux facilitent la réactivité, NgRx structure l'état de l'entreprise et Reactive Forms gère les formulaires complexes robustement.

Le meilleur choix n’est pas toujours d’utiliser la technologie la plus avancée, mais de choisir l’outil le mieux adapté au problème. Commencez simplement, ajoutez de la complexité uniquement lorsque cela est nécessaire et gardez le code lisible.

💬 Notes des lecteurs

0 notes

Écrire une note

Partagez votre avis, une suggestion ou un compliment

Dernières notes

Aucune note pour le moment. Soyez le premier à commenter !