The first problem comes with validation. If one checks and unchecks a checkbox (and no other checkbox is checked), the input is still valid - somehow strange.
I noticed that if the value of languages is an empty array, it passes the Validations.required check.
The second problem comes with the value, which is true if two ore more languages are checked and false otherwise.
Then there is a third problem with the initial value of languages which is just an empty array, but causes all checkboxes to be checked initially (doesn't happen if the initial value is set to a string), although I can not spot any checked attribute in the DOM.
I think the problem is the way you are binding multiple controls to a single FormControl, I believe a FormArray needs to be involved, potentially with a different FormControl storing the result of your checkbox array.
So, is there any guide on how to work with checkbox groups? Any advice or ideas?
Sure, I took a stab at implementing it, I will post the implementation first followed by some notes. You can view it in action at https://plnkr.co/edit/hFU904?p=preview
@Component({
template: `
<template [ngIf]="loading">
Loading languages...
</template>
<template [ngIf]="!loading">
<form [formGroup]="modelForm">
<div [formArrayName]="'languages'" [class.invalid]="!modelForm.controls.selectedLanguages.valid">
<div *ngFor="let language of modelForm.controls.languages.controls; let i = index;" [formGroup]="language">
<input type="checkbox" formControlName="checked" id="language_{{ language.controls.key.value }}">
<label attr.for="language_{{ language.controls.key.value }}">{{ language.controls.value.value }}</label>
</div>
</div>
<hr>
<pre>{{modelForm.controls.selectedLanguages.value | json}}</pre>
</form>
</template>
`
})
export class AppComponent {
loading:boolean = true;
modelForm:FormGroup;
languages:LanguageKeyValues[];
constructor(public formBuilder:FormBuilder){
}
ngOnInit() {
this.translateService.get('languages').subscribe((languages:LanguageKeyValues[]) => {
let languagesControlArray = new FormArray(languages.map((l) => {
return new FormGroup({
key: new FormControl(l.key),
value: new FormControl(l.value),
checked: new FormControl(false),
});
}));
this.modelForm = new FormGroup({
languages: languagesControlArray,
selectedLanguages: new FormControl(this.mapLanguages(languagesControlArray.value), Validators.required)
});
languagesControlArray.valueChanges.subscribe((v) => {
this.modelForm.controls.selectedLanguages.setValue(this.mapLanguages(v));
});
this.loading = false;
});
}
mapLanguages(languages) {
let selectedLanguages = languages.filter((l) => l.checked).map((l) => l.key);
return selectedLanguages.length ? selectedLanguages : null;
}
}
The main difference here is that I merged your model.languages into your modelForm, and am now repeating on the modelForm.languages FormArray in the template.
modelForm.languages has become modelForm.selectedLanguages, and is now a computed value based on the checked values in modelForm.languages. If nothing is selected, modelForm.selectedLanguages is set to null, to fail validation.
modelForm is not instantiated until languages are available, this is mostly personal preference, I'm sure you could asynchronously attach languages and selectedLanguages to your modelForm, but it simplifies things to construct it synchronously.
I took out translateService.get('languages') | async, I noticed some strange behavior with this function being called in the template, and I prefer to unwrap my observables in the component anyway, to capture loading/error states.
It's not as elegant as some native checkbox array form control could be, but it's clean and very flexible. Check out the plunker and let me know if you have any questions!