Appearance
FormArray : gérer des champs de formulaire dynamiques
Les FormArray
sont particulièrement utiles lorsque vous devez gérer une liste dynamique de champs de formulaire. Imaginez que vous créez une liste de courses : vous ne savez pas à l'avance combien d'articles vous allez ajouter, et vous voulez pouvoir en ajouter ou en supprimer facilement.
Comprendre par l'exemple
Prenons un exemple concret : vous gérez un formulaire d'inscription à un événement où les participants peuvent ajouter plusieurs invités. Chaque invité a un nom et une adresse email.
Idée principale
FormArray
est idéal quand vous ne connaissez pas à l'avance le nombre d'éléments que l'utilisateur va ajouter.
Voici comment implémenter cela :
ts
import { Component, inject } from '@angular/core';
import {
FormArray,
FormBuilder,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
@Component({
selector: 'app-event-form',
standalone: true,
templateUrl: './event-form.component.html',
imports: [ReactiveFormsModule],
})
export class EventFormComponent {
private fb = inject(FormBuilder);
eventForm = this.fb.group({
eventName: ['', Validators.required],
guests: this.fb.array([]),
});
get guests() {
return this.eventForm.get('guests') as FormArray;
}
addGuest() {
const guestForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
});
this.guests.push(guestForm);
}
removeGuest(index: number) {
this.guests.removeAt(index);
}
onSubmit() {
if (this.eventForm.valid) {
console.log(this.eventForm.value);
}
}
}
angular-html
<form [formGroup]="eventForm" (ngSubmit)="onSubmit()">
<div>
<label for="eventName">Nom de l'événement</label>
<input id="eventName" formControlName="eventName" />
</div>
<div formArrayName="guests">
@for (guest of guests.controls; track guest; let i = $index) {
<div [formGroupName]="i">
<h3>Invité #{{ i + 1 }}</h3>
<div>
<label>Nom:</label>
<input formControlName="name" />
</div>
<div>
<label>Email:</label>
<input formControlName="email" />
</div>
<button type="button" (click)="removeGuest(i)">
Supprimer l'invité
</button>
</div>
}
</div>
<button type="button" (click)="addGuest()">Ajouter un invité</button>
<button type="submit" [disabled]="!eventForm.valid">Enregistrer</button>
</form>
Comment fonctionne FormArray ?
Analysons chaque partie :
1. Création du FormArray
STRUCTURE
Un FormArray est toujours une propriété d'un FormGroup parent. Il peut contenir des FormControl, FormGroup ou même d'autres FormArray.
ts
eventForm = this.fb.group({
eventName: ['', Validators.required],
guests: this.fb.array([]), // FormArray vide au départ
});
2. Accès au FormArray
Pour manipuler facilement le FormArray, on crée un getter :
ts
get guests() {
return this.eventForm.get('guests') as FormArray;
}
3. Ajout d'éléments
Quand on ajoute un élément, on crée généralement un nouveau FormGroup
:
ts
const guestForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
});
this.guests.push(guestForm);
4. Structure HTML
La directive formArrayName
permet de lier un élément HTML à un FormArray
dans votre formulaire réactif. Elle agit comme un pont entre votre template et la définition du FormArray
dans votre composant.
Voici comment elle fonctionne :
ts
eventForm = this.fb.group({
eventName: [''],
guests: this.fb.array([]) // 'guests' est le nom qu'on utilisera dans formArrayName
});
angular-html
<div formArrayName="guests">
<!-- Ici, Angular sait que 'guests' fait référence au FormArray -->
<!-- Le contenu de cette div a accès au contexte du FormArray -->
</div>
[formGroupName]="$index"
permet d'accéder à chaque FormGroup individuel dans le FormArray. L'index est crucial car il indique à Angular quel élément du tableau nous manipulons.
Voici un exemple détaillé :
ts
// Dans le composant, chaque élément du FormArray est un FormGroup
addGuest() {
const guestForm = this.fb.group({
name: [''],
email: ['']
});
this.guests.push(guestForm);
}
angular-html
<div formArrayName="guests">
@for (guest of guests.controls; track $index) {
<!-- $index représente la position dans le tableau (0, 1, 2, etc.) -->
<div [formGroupName]="$index">
<!-- Maintenant nous sommes dans le contexte du FormGroup spécifique -->
<input formControlName="name">
<input formControlName="email">
</div>
}
</div>
Hiérarchie des directives
La structure hiérarchique est importante :
angular-html
<form [formGroup]="eventForm"> <!-- Niveau 1: FormGroup principal -->
<div formArrayName="guests"> <!-- Niveau 2: FormArray -->
<div [formGroupName]="$index"> <!-- Niveau 3: FormGroup individuel -->
<input formControlName="name"> <!-- Niveau 4: FormControl -->
</div>
</div>
</form>
ATTENTION
Les directives doivent respecter cette hiérarchie. Si vous oubliez un niveau ou changez l'ordre, Angular lancera une erreur.
Problème de tracking avec @for
ATTENTION
Le tracking avec $index
peut causer des problèmes d'affichage lors de la suppression d'éléments au milieu du tableau.
track $index
≈ "la clé de chaque item = sa position".
Si on retire l'élément #1, l'ancien #2 devient #1, etc. Les clés changent, donc Angular recycle les éléments DOM existants au mauvais endroit → valeurs et états (touched/dirty/errors) se mélangent visuellement.
La bonne pratique
Tracker avec une clé stable qui ne change pas quand on insère/supprime :
- soit l'instance du contrôle (
guest
), qui reste la même tant que l'item vit ; - soit un id unique stocké dans la valeur.
La plus simple ici : tracker par l'instance.
angular-html
<div formArrayName="guests">
@for (guest of guests.controls; track guest; let i = $index) {
<div [formGroupName]="i">
<h3>Invité #{{ i + 1 }}</h3>
<div>
<label>Nom:</label>
<input formControlName="name" />
</div>
<div>
<label>Email:</label>
<input formControlName="email" />
</div>
<button type="button" (click)="removeGuest(i)">
Supprimer l'invité
</button>
</div>
}
</div>
Explication de la solution
Remarque : on garde [formGroupName]="i"
(l'index sert à pointer le bon enfant dans le FormArray), mais le track n'est plus basé sur l'index : il est basé sur guest
(l'objet AbstractControl), qui est stable.
Variante "clé métier"
Si vous préférez une clé explicite, ajoutez un id à chaque groupe :
ts
createGuestGroup(): FormGroup {
return this.fb.group({
id: this.fb.nonNullable.control(crypto.randomUUID()),
name: [''],
email: ['']
});
}
angular-html
@for (guest of guests.controls; track guest.value.id; let i = $index) {
<div [formGroupName]="i"> … </div>
}
TL;DR
Ne jamais track $index
pour des listes mutables (insert/suppress/reorder).
Utilise track guest
(référence de contrôle) ou track guest.value.id
.
Votre FormArray reste sain ; c'est juste le rendu qui était mal clé.
Accès aux valeurs et états
Vous pouvez accéder aux valeurs et états à différents niveaux :
ts
// Accès à tout le FormArray
console.log(this.guests.value);
// Accès à un FormGroup spécifique
console.log(this.guests.at(0).value);
// Accès à un contrôle spécifique
console.log(this.guests.at(0).get('name').value);
// Vérification de la validité
console.log(this.guests.valid); // Vérifie tout le FormArray
console.log(this.guests.at(0).valid); // Vérifie un FormGroup spécifique
ASTUCE
Utilisez la méthode at()
plutôt que l'accès direct par index (controls[index]
) car elle est plus sûre et typée.
5. Méthodes utiles du FormArray
Voici les principales méthodes que vous pouvez utiliser avec FormArray :
ts
// Ajouter un contrôle à la fin
this.guests.push(newFormGroup);
// Ajouter un contrôle à une position spécifique
this.guests.insert(index, newFormGroup);
// Supprimer un contrôle à un index
this.guests.removeAt(index);
// Supprimer tous les contrôles
this.guests.clear();
// Obtenir le nombre de contrôles
const length = this.guests.length;
Exemple pratique : Validation du nombre d'invités
Voici comment ajouter une validation sur le nombre d'invités :
ts
addGuest() {
if (this.guests.length < 5) { // Maximum 5 invités
const guestForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
});
this.guests.push(guestForm);
}
}
angular-html
<button
type="button"
(click)="addGuest()"
[disabled]="guests.length >= 5">
Ajouter un invité
</button>
@if (guests.length >= 5) {
<p class="error">Maximum 5 invités autorisés</p>
}
Accès aux valeurs
Pour récupérer toutes les valeurs du FormArray :
ts
onSubmit() {
if (this.eventForm.valid) {
const guestList = this.guests.value; // Tableau des valeurs
console.log(guestList);
// [{name: "John", email: "[email protected]"}, ...]
}
}