TL; DR
- Tôi thích sử dụng FormGroup để điền danh sách hộp kiểm
- Viết trình xác thực tùy chỉnh để kiểm tra ít nhất một hộp kiểm đã được chọn
- Ví dụ làm việc https://stackblitz.com/edit/angular-validate-at-least-one-checkbox-was-selected
Điều này đôi khi cũng khiến tôi bất ngờ vì vậy tôi đã thử cả hai cách tiếp cận FormArray và FormGroup.
Hầu hết thời gian, danh sách hộp kiểm được điền trên máy chủ và tôi nhận được thông qua API. Nhưng đôi khi bạn sẽ có một tập hợp hộp kiểm tĩnh với giá trị được xác định trước của bạn. Với mỗi trường hợp sử dụng, FormArray hoặc FormGroup tương ứng sẽ được sử dụng.
Về cơ bản FormArray
là một biến thể của FormGroup
. Sự khác biệt chính là dữ liệu của nó được tuần tự hóa dưới dạng một mảng (trái ngược với việc được tuần tự hóa như một đối tượng trong trường hợp của FormGroup). Điều này có thể đặc biệt hữu ích khi bạn không biết có bao nhiêu điều khiển sẽ hiện diện trong nhóm, chẳng hạn như biểu mẫu động.
Để đơn giản hơn, hãy tưởng tượng bạn có một mẫu sản phẩm tạo đơn giản với
- Một hộp văn bản tên sản phẩm bắt buộc.
- Một danh sách các danh mục để lựa chọn, cần có ít nhất một danh mục được kiểm tra. Giả sử danh sách sẽ được truy xuất từ máy chủ.
Đầu tiên, tôi thiết lập một biểu mẫu chỉ có tên sản phẩm formControl. Nó là một trường bắt buộc.
this.form = this.formBuilder.group({
name: ["", Validators.required]
});
Vì danh mục đang hiển thị động, tôi sẽ phải thêm những dữ liệu này vào biểu mẫu sau khi dữ liệu đã sẵn sàng.
this.getCategories().subscribe(categories => {
this.form.addControl("categoriesFormArr", this.buildCategoryFormArr(categories));
this.form.addControl("categoriesFormGroup", this.buildCategoryFormGroup(categories));
})
Có hai cách tiếp cận để xây dựng danh sách danh mục.
1. Mảng biểu mẫu
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>
Điều này buildCategoryFormGroup
sẽ trả lại cho tôi một FormArray. Nó cũng lấy danh sách giá trị đã chọn làm đối số, vì vậy Nếu bạn muốn sử dụng lại biểu mẫu để chỉnh sửa dữ liệu, nó có thể hữu ích. Với mục đích tạo form sản phẩm mới nên chưa thể áp dụng được.
Lưu ý rằng khi bạn cố gắng truy cập các giá trị formArray. Nó sẽ trông như thế nào [false, true, true]
. Để có được danh sách các id đã chọn, cần phải thực hiện thêm một chút để kiểm tra từ danh sách nhưng dựa trên chỉ số mảng. Nghe có vẻ không tốt với tôi nhưng nó hoạt động.
get categoriesFormArraySelectedIds(): string[] {
return this.categories
.filter((cat, catIdx) => this.categoriesFormArr.controls.some((control, controlIdx) => catIdx === controlIdx && control.value))
.map(cat => cat.id);
}
Đó là lý do tại sao tôi sử dụng FormGroup
cho vấn đề đó
2. Nhóm biểu mẫu
Sự khác biệt của formGroup là nó sẽ lưu trữ dữ liệu biểu mẫu dưới dạng đối tượng, yêu cầu một khóa và một điều khiển biểu mẫu. Vì vậy, bạn nên đặt khóa là CategoryId và sau đó chúng ta có thể truy xuất nó sau.
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>
Giá trị của nhóm biểu mẫu sẽ giống như sau:
{
"category1": false,
"category2": true,
"category3": true,
}
Nhưng thường thì chúng ta chỉ muốn lấy danh sách các CategoryIds dưới dạng ["category2", "category3"]
. Tôi cũng phải viết thư để lấy những dữ liệu này. Tôi thích cách tiếp cận này hơn so với formArray, bởi vì tôi thực sự có thể lấy giá trị từ chính biểu mẫu.
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. Trình xác thực tùy chỉnh để kiểm tra ít nhất một hộp kiểm đã được chọn
Tôi đã thực hiện trình xác thực để kiểm tra ít nhất hộp kiểm X đã được chọn, theo mặc định, nó sẽ chỉ kiểm tra với một hộp kiểm.
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;
};
}