I don't get, how checkbox groups should be handled in a model driven form in angular2.
The model has a property languages
which I instantiate like this:
this.model = {
languages: []
};
Using FormBuilder
to create the form:
this.modelForm = this.formBuilder.group({
'languages': [model.languages, Validators.required],
});
And the template:
<div *ngFor="let language of translateService.get('languages') | async | key_value">
<input type="checkbox" name="languages[]" formControlName="languages" value="{{ language.key }}" id="language_{{ language.key }}">
<label attr.for="language_{{ language.key }}">{{ language.value }}</label>
</div>
The div
is needed for styling purposes (custom checkbox), key_value
makes keys and values of the language available in the loop, obviously.
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.
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'm working with the latest ionic beta (2.0.0-beta.10) which uses angular/core version 2.0.0-rc.4 and angular/forms version 0.2.0.
So, is there any guide on how to work with checkbox groups? Any advice or ideas?
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!
If you check a checkbox and then uncheck it, the FormControl will still show it as valid, even though it is unchecked. It may happen that it takes only the true and false values.You can try this sample of code as it worked for me -
mustBeChecked(control: FormControl): {[key: string]: string} {
if (!control.value) {
return {mustBeCheckedError: 'Must be checked'};
} else {
return null;
}
}
You can also refer to this plunker
There is currently an open issue here where click event triggers before changing the value. Refer to the github link here.Hope It may help you.
There are different workarounds for multiple checkboxes like using
event.target.checked
instead of the value from the model.
YOu can use it as in this sample -
<input type="checkbox"
(change)="expression && expression.Option1=$event.target.checked ? true : undefiend"
[ngModel]="expression?.Option1">
<input type="checkbox"
(change)="expression && expression.Option2=$event.target.checked ? true : undefiend"
[ngModel]="expression?.Option2">
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With