TL; DR
- Je préfère utiliser FormGroup pour remplir la liste des cases à cocher
- Ecrire un validateur personnalisé pour vérifier qu'au moins une case a été cochée
- Exemple de travail https://stackblitz.com/edit/angular-validate-at-least-one-checkbox-was-selected
Cela m'a également frappé parfois, alors j'ai essayé les approches FormArray et FormGroup.
La plupart du temps, la liste des cases à cocher était remplie sur le serveur et je l'ai reçue via l'API. Mais parfois, vous aurez un ensemble statique de cases à cocher avec votre valeur prédéfinie. Avec chaque cas d'utilisation, le FormArray ou FormGroup correspondant sera utilisé.
Fondamentalement, FormArray
est une variante de FormGroup
. La principale différence est que ses données sont sérialisées en tant que tableau (par opposition à être sérialisées en tant qu'objet dans le cas de FormGroup). Cela peut être particulièrement utile lorsque vous ne savez pas combien de contrôles seront présents dans le groupe, comme les formulaires dynamiques.
Par souci de simplicité, imaginez que vous disposez d'un formulaire de création de produit simple avec
- Une zone de texte de nom de produit obligatoire.
- Une liste de catégories à sélectionner, dont au moins une doit être vérifiée. Supposons que la liste sera extraite du serveur.
Tout d'abord, j'ai mis en place un formulaire avec uniquement le nom de produit formControl. C'est un champ obligatoire.
this.form = this.formBuilder.group({
name: ["", Validators.required]
});
Étant donné que la catégorie est rendue dynamiquement, je devrai ajouter ces données dans le formulaire plus tard, une fois les données prêtes.
this.getCategories().subscribe(categories => {
this.form.addControl("categoriesFormArr", this.buildCategoryFormArr(categories));
this.form.addControl("categoriesFormGroup", this.buildCategoryFormGroup(categories));
})
Il existe deux approches pour constituer la liste des catégories.
1. Form Array
buildCategoryFormArr(categories: ProductCategory[], selectedCategoryIds: string[] = []): FormArray {
const controlArr = categories.map(category => {
let isSelected = selectedCategoryIds.some(id => id === category.id);
return this.formBuilder.control(isSelected);
})
return this.formBuilder.array(controlArr, atLeastOneCheckboxCheckedValidator())
}
<div *ngFor="let control of categoriesFormArr?.controls; let i = index" class="checkbox">
<label><input type="checkbox" [formControl]="control" />
{{ categories[i]?.title }}
</label>
</div>
Cela buildCategoryFormGroup
me renverra un FormArray. Il prend également une liste de valeurs sélectionnées comme argument, donc si vous souhaitez réutiliser le formulaire pour modifier les données, cela peut être utile. Dans le but de créer un nouveau formulaire de produit, il n'est pas encore applicable.
A noté que lorsque vous essayez d'accéder aux valeurs formArray. Cela ressemblera à [false, true, true]
. Pour obtenir une liste des identifiants sélectionnés, il a fallu un peu plus de travail pour vérifier dans la liste, mais en fonction de l'index du tableau. Cela ne me semble pas bon mais ça marche.
get categoriesFormArraySelectedIds(): string[] {
return this.categories
.filter((cat, catIdx) => this.categoriesFormArr.controls.some((control, controlIdx) => catIdx === controlIdx && control.value))
.map(cat => cat.id);
}
C'est pourquoi je suis venu utiliser FormGroup
pour cette question
2. Groupe de formulaires
La différence du formGroup est qu'il stockera les données du formulaire en tant qu'objet, ce qui nécessitait une clé et un contrôle de formulaire. C'est donc la bonne idée de définir la clé comme categoryId, puis nous pourrons la récupérer plus tard.
buildCategoryFormGroup(categories: ProductCategory[], selectedCategoryIds: string[] = []): FormGroup {
let group = this.formBuilder.group({}, {
validators: atLeastOneCheckboxCheckedValidator()
});
categories.forEach(category => {
let isSelected = selectedCategoryIds.some(id => id === category.id);
group.addControl(category.id, this.formBuilder.control(isSelected));
})
return group;
}
<div *ngFor="let item of categories; let i = index" class="checkbox">
<label><input type="checkbox" [formControl]="categoriesFormGroup?.controls[item.id]" /> {{ categories[i]?.title }}
</label>
</div>
La valeur du groupe de formulaires ressemblera à ceci:
{
"category1": false,
"category2": true,
"category3": true,
}
Mais le plus souvent, nous voulons obtenir uniquement la liste des ID de catégorie ["category2", "category3"]
. Je dois aussi écrire un get pour prendre ces données. J'aime mieux cette approche par rapport à formArray, car je pourrais en fait prendre la valeur du formulaire lui-même.
get categoriesFormGroupSelectedIds(): string[] {
let ids: string[] = [];
for (var key in this.categoriesFormGroup.controls) {
if (this.categoriesFormGroup.controls[key].value) {
ids.push(key);
}
else {
ids = ids.filter(id => id !== key);
}
}
return ids;
}
3. Un validateur personnalisé pour cocher au moins une case a été coché
J'ai fait en sorte que le validateur vérifie qu'au moins X case à cocher était cochée, par défaut, il ne vérifiera qu'une seule case.
export function atLeastOneCheckboxCheckedValidator(minRequired = 1): ValidatorFn {
return function validate(formGroup: FormGroup) {
let checked = 0;
Object.keys(formGroup.controls).forEach(key => {
const control = formGroup.controls[key];
if (control.value === true) {
checked++;
}
});
if (checked < minRequired) {
return {
requireCheckboxToBeChecked: true,
};
}
return null;
};
}