TL; DR
- Wolę używać FormGroup, aby wypełnić listę pól wyboru
- Napisz niestandardowy walidator, aby sprawdzić, czy wybrano co najmniej jedno pole wyboru
- Przykład roboczy https://stackblitz.com/edit/angular-validate-at-least-one-checkbox-was-selected
To również mnie czasami uderzało, więc wypróbowałem zarówno podejście FormArray, jak i FormGroup.
W większości przypadków lista checkboxów znajdowała się na serwerze i otrzymywałem ją przez API. Ale czasami będziesz mieć statyczny zestaw pól wyboru ze wstępnie zdefiniowaną wartością. W każdym przypadku użycia zostanie użyty odpowiedni FormArray lub FormGroup.
Zasadniczo FormArray
jest wariantem FormGroup
. Kluczową różnicą jest to, że jego dane są serializowane jako tablica (w przeciwieństwie do serializacji jako obiektu w przypadku FormGroup). Może to być szczególnie przydatne, gdy nie wiesz, ile kontrolek będzie obecnych w grupie, takich jak formularze dynamiczne.
Ze względu na prostotę wyobraź sobie, że masz prostą formę produktu za pomocą
- Jedno wymagane pole tekstowe nazwy produktu.
- Lista kategorii do wyboru, wymagająca sprawdzenia co najmniej jednej. Załóżmy, że lista zostanie pobrana z serwera.
Najpierw skonfigurowałem formularz z tylko nazwą produktu formControl. To jest wymagane pole.
this.form = this.formBuilder.group({
name: ["", Validators.required]
});
Ponieważ kategoria jest renderowana dynamicznie, będę musiał dodać te dane do formularza później, gdy dane będą gotowe.
this.getCategories().subscribe(categories => {
this.form.addControl("categoriesFormArr", this.buildCategoryFormArr(categories));
this.form.addControl("categoriesFormGroup", this.buildCategoryFormGroup(categories));
})
Istnieją dwa podejścia do tworzenia listy kategorii.
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>
To buildCategoryFormGroup
zwróci mi FormArray. Pobiera również listę wybranych wartości jako argument, więc jeśli chcesz ponownie użyć formularza do edycji danych, może to być pomocne. W celu stworzenia nowego formularza produktu nie ma jeszcze zastosowania.
Zauważyłem, że podczas próby uzyskania dostępu do wartości formArray. Będzie to wyglądało [false, true, true]
. Aby uzyskać listę wybranych identyfikatorów, sprawdzenie z listy wymagało nieco więcej pracy, ale w oparciu o indeks tablicy. Nie brzmi to dla mnie dobrze, ale działa.
get categoriesFormArraySelectedIds(): string[] {
return this.categories
.filter((cat, catIdx) => this.categoriesFormArr.controls.some((control, controlIdx) => catIdx === controlIdx && control.value))
.map(cat => cat.id);
}
Dlatego wymyśliłem używanie FormGroup
w tym celu
2. Grupa formularzy
Różnica w formGroup polega na tym, że przechowuje dane formularza jako obiekt, który wymagał klucza i kontrolki formularza. Dlatego dobrym pomysłem jest ustawienie klucza jako categoryId, a później możemy go odzyskać.
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>
Wartość grupy formularzy będzie wyglądać następująco:
{
"category1": false,
"category2": true,
"category3": true,
}
Ale najczęściej chcemy uzyskać tylko listę categoryIds jako ["category2", "category3"]
. Muszę też napisać, żeby wziąć te dane. Takie podejście podoba mi się bardziej w porównaniu z formArray, ponieważ faktycznie mógłbym wziąć wartość z samego formularza.
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. Własny walidator, aby zaznaczyć przynajmniej jedno pole wyboru
Zrobiłem walidator, aby sprawdzał co najmniej X checkbox jest zaznaczony, domyślnie będzie sprawdzał tylko z jednym checkboxem.
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;
};
}