TL; DR
- Prefiero usar FormGroup para completar la lista de casillas de verificación
- Escriba un validador personalizado para comprobar que se seleccionó al menos una casilla de verificación
- Ejemplo de trabajo https://stackblitz.com/edit/angular-validate-at-least-one-checkbox-was-selected
Esto también me sorprendió a veces, así que probé los enfoques FormArray y FormGroup.
La mayoría de las veces, la lista de casillas de verificación se completó en el servidor y la recibí a través de API. Pero a veces tendrá un conjunto estático de casillas de verificación con su valor predefinido. Con cada caso de uso, se utilizará el FormArray o FormGroup correspondiente.
Básicamente FormArray
es una variante de FormGroup
. La diferencia clave es que sus datos se serializan como una matriz (en lugar de ser serializados como un objeto en el caso de FormGroup). Esto puede ser especialmente útil cuando no sabe cuántos controles estarán presentes dentro del grupo, como formularios dinámicos.
En aras de la simplicidad, imagine que tiene un formulario de creación de producto simple con
- Un cuadro de texto de nombre de producto obligatorio.
- Una lista de categorías para seleccionar, requiere al menos una para ser marcada. Suponga que la lista se recuperará del servidor.
Primero, configuré un formulario con solo el nombre del producto formControl. Es un campo obligatorio.
this.form = this.formBuilder.group({
name: ["", Validators.required]
});
Dado que la categoría se está procesando dinámicamente, tendré que agregar estos datos al formulario más tarde, una vez que los datos estén listos.
this.getCategories().subscribe(categories => {
this.form.addControl("categoriesFormArr", this.buildCategoryFormArr(categories));
this.form.addControl("categoriesFormGroup", this.buildCategoryFormGroup(categories));
})
Hay dos enfoques para crear la lista de categorías.
1. Matriz de formas
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>
Esto buildCategoryFormGroup
me devolverá un FormArray. También toma una lista de valores seleccionados como argumento, por lo que si desea reutilizar el formulario para editar datos, podría ser útil. Para el propósito de crear un nuevo formulario de producto, aún no es aplicable.
Observó que cuando intenta acceder a los valores de formArray. Se verá así [false, true, true]
. Para obtener una lista de ID seleccionados, se requirió un poco más de trabajo para verificar de la lista, pero según el índice de la matriz. No me suena bien, pero funciona.
get categoriesFormArraySelectedIds(): string[] {
return this.categories
.filter((cat, catIdx) => this.categoriesFormArr.controls.some((control, controlIdx) => catIdx === controlIdx && control.value))
.map(cat => cat.id);
}
Por eso se me ocurrió usar FormGroup
para el caso
2. Formar grupo
La diferencia de formGroup es que almacenará los datos del formulario como el objeto, que requiere una clave y un control de formulario. Por lo tanto, es una buena idea establecer la clave como categoryId y luego podemos recuperarla más tarde.
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>
El valor del grupo de formularios se verá así:
{
"category1": false,
"category2": true,
"category3": true,
}
Pero la mayoría de las veces queremos obtener solo la lista de categoryIds como ["category2", "category3"]
. También tengo que escribir un get para tomar estos datos. Me gusta más este enfoque en comparación con formArray, porque en realidad podría tomar el valor del formulario en sí.
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. Se seleccionó un validador personalizado para marcar al menos una casilla de verificación
Hice que el validador marcara al menos X la casilla de verificación seleccionada, de forma predeterminada, solo se verificará con una casilla de verificación.
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;
};
}