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 attentedes 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
tracken 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.