TL; DR
- Eu prefiro usar FormGroup para preencher a lista de caixas de seleção
- Escreva um validador personalizado para verificar se pelo menos uma caixa de seleção foi selecionada
- Exemplo de trabalho https://stackblitz.com/edit/angular-validate-at-least-one-checkbox-was-selected
Isso também me surpreendeu algumas vezes, então tentei as abordagens FormArray e FormGroup.
Na maioria das vezes, a lista de caixas de seleção era preenchida no servidor e eu a recebia por meio da API. Mas às vezes você terá um conjunto estático de caixa de seleção com seu valor predefinido. Com cada caso de uso, o FormArray ou FormGroup correspondente será usado.
Basicamente, FormArray
é uma variante de FormGroup
. A principal diferença é que seus dados são serializados como um array (em vez de serem serializados como um objeto no caso de FormGroup). Isso pode ser especialmente útil quando você não sabe quantos controles estarão presentes no grupo, como formulários dinâmicos.
Para simplificar, imagine que você tem um formulário de criação de produto simples com
- Uma caixa de texto de nome de produto necessária.
- Uma lista de categorias para selecionar, exigindo que pelo menos uma seja verificada. Suponha que a lista será recuperada do servidor.
Primeiro, configurei um formulário com apenas o nome de produto formControl. É um campo obrigatório.
this.form = this.formBuilder.group({
name: ["", Validators.required]
});
Como a categoria é renderizada dinamicamente, terei que adicionar esses dados ao formulário mais tarde, depois que os dados estiverem prontos.
this.getCategories().subscribe(categories => {
this.form.addControl("categoriesFormArr", this.buildCategoryFormArr(categories));
this.form.addControl("categoriesFormGroup", this.buildCategoryFormGroup(categories));
})
Existem duas abordagens para construir a lista de categorias.
1. Matriz de formulário
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>
Isso buildCategoryFormGroup
me retornará um FormArray. Ele também leva uma lista de valores selecionados como um argumento. Se você quiser reutilizar o formulário para editar dados, pode ser útil. Para fins de criação de um novo formulário de produto, ainda não é aplicável.
Observe que, ao tentar acessar os valores de formArray. Será parecido [false, true, true]
. Para obter uma lista de id selecionados, é necessário um pouco mais de trabalho para verificar a lista, mas com base no índice da matriz. Não parece bom para mim, mas 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 isso que comecei a usar FormGroup
para esse assunto
2. Grupo de forma
A diferença do formGroup é que ele armazena os dados do formulário como o objeto, o que requer uma chave e um controle de formulário. Portanto, é uma boa ideia definir a chave como categoryId e então podemos recuperá-la mais 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>
O valor do grupo de formulários será semelhante a:
{
"category1": false,
"category2": true,
"category3": true,
}
Mas, na maioria das vezes, queremos obter apenas a lista de categoryIds como ["category2", "category3"]
. Eu também tenho que escrever um get para obter esses dados. Eu gosto mais dessa abordagem em comparação com formArray, porque eu poderia realmente pegar o valor do próprio formulário.
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. O validador personalizado para verificar se pelo menos uma caixa de seleção foi selecionada
Eu fiz o validador marcar pelo menos a caixa de seleção X selecionada, por padrão, ele marcará apenas uma caixa de seleção.
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;
};
}