TL; DR
- Я предпочитаю использовать FormGroup для заполнения списка флажков
- Напишите собственный валидатор для проверки хотя бы одного флажка
- Рабочий пример https://stackblitz.com/edit/angular-validate-at-least-one-checkbox-was-selected
Иногда это также поражало меня, поэтому я попробовал оба подхода - FormArray и FormGroup.
В большинстве случаев список флажков был заполнен на сервере, и я получал его через API. Но иногда у вас будет статический набор флажков с вашим предопределенным значением. В каждом варианте использования будет использоваться соответствующий FormArray или FormGroup.
В основном FormArray
это вариант FormGroup
. Ключевое отличие состоит в том, что его данные сериализуются как массив (в отличие от сериализации как объекта в случае FormGroup). Это может быть особенно полезно, когда вы не знаете, сколько элементов управления будет присутствовать в группе, например, динамических форм.
Для простоты представьте, что у вас есть простая форма создания продукта с
- Одно обязательное текстовое поле с названием продукта.
- Список категорий для выбора, требующий проверки хотя бы одной. Предположим, список будет получен с сервера.
Во-первых, я создал форму только с названием продукта formControl. Это обязательное поле.
this.form = this.formBuilder.group({
name: ["", Validators.required]
});
Поскольку категория динамически отображается, мне придется добавить эти данные в форму позже, когда данные будут готовы.
this.getCategories().subscribe(categories => {
this.form.addControl("categoriesFormArr", this.buildCategoryFormArr(categories));
this.form.addControl("categoriesFormGroup", this.buildCategoryFormGroup(categories));
})
Есть два подхода к созданию списка категорий.
1. Сформировать массив
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>
это buildCategoryFormGroup
вернет мне FormArray. Он также принимает список выбранных значений в качестве аргумента, поэтому, если вы хотите повторно использовать форму для редактирования данных, это может быть полезно. Для создания новой формы продукта он пока не применим.
Заметил, что при попытке доступа к значениям formArray. Это будет похоже [false, true, true]
. Чтобы получить список выбранных идентификаторов, потребовалось немного больше работы для проверки из списка, но на основе индекса массива. Мне это не нравится, но работает.
get categoriesFormArraySelectedIds(): string[] {
return this.categories
.filter((cat, catIdx) => this.categoriesFormArr.controls.some((control, controlIdx) => catIdx === controlIdx && control.value))
.map(cat => cat.id);
}
Вот почему я пришел к использованию FormGroup
в этом отношении
2. Группа форм
Отличие formGroup в том, что данные формы будут храниться в виде объекта, для чего требуется ключ и элемент управления формой. Поэтому рекомендуется установить ключ как categoryId, а затем мы сможем получить его позже.
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>
Значение группы форм будет выглядеть так:
{
"category1": false,
"category2": true,
"category3": true,
}
Но чаще всего мы хотим получить только список categoryIds как ["category2", "category3"]
. Я также должен написать прием, чтобы взять эти данные. Мне этот подход больше нравится по сравнению с formArray, потому что я действительно мог взять значение из самой формы.
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. Пользовательский валидатор для проверки хотя бы одного флажка был выбран.
Я сделал валидатор, чтобы проверить, по крайней мере, флажок X был выбран, по умолчанию он будет проверять только один флажок.
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;
};
}