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,
}
Але найчастіше ми хочемо отримати лише список ідентифікаторів категорії як ["category2", "category3"]
. Я також повинен написати get, щоб взяти ці дані. Мені подобається такий підхід краще порівняно з 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;
};
}