Formulaires dynamiques avec Angular
Posted on mar. 05 février 2019 in Frontend
Présentation
Il s'agit tout simplement d'ajouter ou de supprimer des éléments de formulaire de manière interactive. Le formulaire n'est donc pas figé dans le temps et peut évoluer en fonction du contexte.
Ce mode de fonctionnement est très utile lorsque le nombre de champs d'un formulaire n'est pas toujours prédictible et c'est souvent le cas lorsqu'on utilise des base de données de type NOSQL, comme MongoDB par exemple, où le nombre de propriétés peut varier en fonction du document.
C'est aussi le cas lorsqu'on utilise des listes d'objets dont la taille est variable comme par exemple les emails d'un contact ou ses numéros de téléphone. Lors de la création ou de la modification d'un contact, il serait utile de pouvoir ajouter ou retirer ces champs en fonction des besoins.
Les formulaires de type Questions-Réponses se prêtent bien également à ce genre de manipulation et de manière plus générale tous les formulaires comportant un nombre important de champs.
Etude de cas
Imaginons par exemple une application comportant un formulaire de création ou de modification des réalisateurs de cinéma. Le nombre de films réalisés diffère en fonction de chaque metteur en scène et c'est bien ici l'occasion d'appliquer un formulaire dynamique.
Voici tout d'abord les objets du domaine
Objets du modèle : director.ts
Le modèle a été simplifié à l'extrême pour n'exposer que les propriétés les plus représentatives : id, lastname, firstname et listes de movies.
La classe Movie
, quant à elle, ne comporte que deux propriétés : un titre et un genre.
export class Director {
constructor(
private _id: number,
private _lastname: string,
private _firstname: string,
private _movies: Movie[]
) { }
get id() { return this._id; }
set id(id: number) { this._id = id; }
get firstname() { return this._firstname; }
set firstname(firstname: string) { this._firstname = firstname; }
get lastname() { return this._lastname; }
set lastname(name: string) { this._lastname = name; }
get movies() { return this._movies; }
set movies(movies: Movie[]) { this._movies = movies; }
}
export class Movie {
constructor(private _title: string, private _genre: string) { }
get title() { return this._title; }
set genre(genre: string) { this._genre = genre; }
}
export enum Genre {
"ADVENTURE",
"COMEDY",
"DRAMA",
"SCI-FI",
"HORROR",
}
Initialisation du composant : director.component.ts
export class DirectorFormComponent implements OnInit {
directorForm: FormGroup;
constructor(
private fb: FormBuilder,
private service: DirectorService,
@Inject(GENRES_DI) private genres: string[]
) {
this.directorForm = this.createForm();
}
ngOnInit() {
this.load();
}
...
Les données du formulaire sont récupérées par l'intermédiaire du service dans la méthode load
.
Création du formulaire
Passons ensuite à la création du formulaire proprement dit. La ligne qui nous intéresse ici est movies: this.fb.array([ ])
. Elle permet d'initialiser le tableau qui contiendra nos champs « dynamiques ».
FormBuilder
(this.fb
) est une classe utilitaire simplifiant la création d'un formulaire. La méthode array()
renvoie le FormArray
au sein duquel se logeront nos FormGroup
de films.
createForm() {
return this.fb.group({
firstname: ['', Validators.required],
lastname: ['', Validators.required],
movies: this.fb.array([
])
});
}
Getters
Le composant dispose d'un certain nombre de getters pour faciliter l'accès aux champs de base :
get firstname() {return this.directorForm.get('firstname');}
get lastname() {return this.directorForm.get('lastname');}
get movies() { return this.directorForm.get('movies') as FormArray; }
Gestion dynamique
Passons enfin aux méthodes dédiées à la gestion du formulaire dynamique :
setMovie(movie: Movie) {
this.movies.push(this.fb.group({ title: movie.title, genre: movie.genre }));
}
addMovie() {
this.movies.push(this.fb.group({ title: '', genre: '' }));
}
removeMovie(index: number) {
this.movies.removeAt(index);
}
resetMovies() {
while (this.movies.length > 0) {
this.movies.removeAt(0);
}
}
Le formArray comporte un certain nombre d'objets de type FormGroup
. addMovie
permet d'ajouter un nouveau groupe de formulaire vide. Cette méthode sera utilisée lors de l'ajout dynamique d'un film.
setMovie
permet de pré-remplir les champs correspondant aux films du réalisateur courant. Cette méthode sera appelée autant de fois que de films réalisés.
Les deux dernières méthodes permettent respectivement de supprimer un film particulier et l'ensemble des films.
Le template html
<form [formGroup]="directorForm" (ngSubmit)="onSubmit(directorForm.value)">
...
<div class="form-array" formArrayName="movies">
<div *ngFor="let movie of movies.controls; let i=index">
<div [formGroupName]="i" class="form-group">
<label>Movie {{i+1}} <input type="text" formControlName="title"></label>
<select id="genre" formControlName="genre">
<option *ngFor="let g of genres" [value]="g">{{g}}</option>
</select>
<button type="button" (click)="removeMovie(i)">Remove</button>
</div>
</div>
<div class="adder">
<button type="button" (click)="addMovie()">Add movie</button>
</div>
</div>
<div class="action-bar">
<button type="reset">Reset</button>
<button type="submit" [disabled]="directorForm.invalid">Save</button>
</div>
</form>
Seuls sont représentés ici les éléments dédiés aux interactions.
La directive formArrayName
permet d'accéder au formArray correspondant dans le formulaire, en l'occurrence, la valeur de la propriété movies
.
L'élément clé à retenir ici est qu'à l'intérieur de ce formArray, le nom de chaque FormGroup est représenté par la valeur de l'index i
dans la boucle de parcours des films du réalisateur.
Conclusion
Comme on le voit, l'implémentation d'un formulaire dynamique est assez triviale et les occasions d'utiliser cette fonctionnalité sont en fait très nombreuses. Son bénéfice est largement supérieur à l'effort consenti et il serait dommage de s'en passer compte tenu de ses bienfaits en matière d'expérience utilisateur.
Pour en savoir plus :