Découvrez les nouveautés d'Angular 20 en quelques minutes

Angular 20 arrive avec plusieurs nouveautés et stabilisation des API: Zoneless, les APIs resource() et httpResource(), un nouveau runner de tests, etc. La vidéo vous donne un aperçu de ces nouveautés.

Abonnez-vous à notre chaîne

Pour profiter des prochaines vidéos sur Angular, abonnez-vous à la nouvelle chaîne YouTube !

Skip to content

Vous souhaitez recevoir de l'aide sur ce sujet ? rejoignez la communauté Angular.fr sur Discord.

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]"}, ...]
  }
}

Exemple complet