TL; DR
- Preferisco utilizzare FormGroup per popolare l'elenco delle caselle di controllo
- Scrivi un validatore personalizzato per verificare che almeno una casella di controllo sia stata selezionata
- Esempio funzionante https://stackblitz.com/edit/angular-validate-at-least-one-checkbox-was-selected
Anche questo mi ha colpito a volte, quindi ho provato entrambi gli approcci FormArray e FormGroup.
Il più delle volte, l'elenco delle caselle di controllo è stato compilato sul server e l'ho ricevuto tramite API. Ma a volte avrai un insieme statico di caselle di controllo con il tuo valore predefinito. Con ogni caso d'uso, verrà utilizzato il FormArray o FormGroup corrispondente.
Fondamentalmente FormArray
è una variante di FormGroup
. La differenza fondamentale è che i suoi dati vengono serializzati come un array (invece di essere serializzati come un oggetto nel caso di FormGroup). Ciò potrebbe essere particolarmente utile quando non sai quanti controlli saranno presenti all'interno del gruppo, come i moduli dinamici.
Per semplicità, immagina di avere un semplice modulo di creazione del prodotto con
- Una casella di testo del nome del prodotto richiesta.
- Un elenco di categorie da cui selezionare, richiedeva che almeno una fosse selezionata. Supponiamo che l'elenco verrà recuperato dal server.
Per prima cosa, ho impostato un modulo con solo il nome del prodotto formControl. È un campo obbligatorio.
this.form = this.formBuilder.group({
name: ["", Validators.required]
});
Poiché la categoria viene visualizzata in modo dinamico, dovrò aggiungere questi dati nel modulo più tardi, dopo che i dati erano pronti.
this.getCategories().subscribe(categories => {
this.form.addControl("categoriesFormArr", this.buildCategoryFormArr(categories));
this.form.addControl("categoriesFormGroup", this.buildCategoryFormGroup(categories));
})
Esistono due approcci per creare l'elenco delle categorie.
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>
Questo buildCategoryFormGroup
mi restituirà un FormArray. Accetta anche un elenco di valori selezionati come argomento, quindi se vuoi riutilizzare il modulo per modificare i dati, potrebbe essere utile. Allo scopo di creare un nuovo modulo di prodotto, non è ancora applicabile.
Notato che quando si tenta di accedere ai valori di formArray. Sembrerà [false, true, true]
. Per ottenere un elenco di ID selezionati, è necessario un po 'più di lavoro per controllare dall'elenco, ma in base all'indice dell'array. Non mi suona bene ma funziona.
get categoriesFormArraySelectedIds(): string[] {
return this.categories
.filter((cat, catIdx) => this.categoriesFormArr.controls.some((control, controlIdx) => catIdx === controlIdx && control.value))
.map(cat => cat.id);
}
Ecco perché ho pensato di usare FormGroup
per quella materia
2. Modulo di gruppo
La differenza del formGroup è che memorizzerà i dati del modulo come oggetto, che richiede una chiave e un controllo del modulo. Quindi è una buona idea impostare la chiave come categoryId e quindi possiamo recuperarla in seguito.
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>
Il valore del gruppo del modulo sarà simile a:
{
"category1": false,
"category2": true,
"category3": true,
}
Ma molto spesso vogliamo ottenere solo l'elenco di ID di categoria come ["category2", "category3"]
. Devo anche scrivere un get per prendere questi dati. Mi piace questo approccio meglio rispetto al formArray, perché potrei effettivamente prendere il valore dal modulo stesso.
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. È stato selezionato un validatore personalizzato per controllare almeno una casella di controllo
Ho fatto in modo che il validatore controllasse almeno X che fosse selezionata la casella di controllo, per impostazione predefinita controllerà solo una casella di controllo.
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;
};
}