Given a list of checkboxes bound to the same formControlName
, how can I produce an array of checkbox values bound to the formControl
, rather than s
I don't see a solution here that completely answers the question using reactive forms to its fullest extent so here's my solution for the same.
Here's the pith of the detailed explanation along with a StackBlitz example.
FormArray
for the checkboxes and initialize the form.valueChanges
observable is perfect for when you want the form to display something but store something else in the component. Map the true
/false
values to the desired values here.false
values at the time of submission.valueChanges
observable.Use FormArray to define the form
As already mentioned in the answer marked as correct. FormArray
is the way to go in such cases where you would prefer to get the data in an array. So the first thing you need to do is create the form.
checkboxGroup: FormGroup;
checkboxes = [{
name: 'Value 1',
value: 'value-1'
}, {
name: 'Value 2',
value: 'value-2'
}];
this.checkboxGroup = this.fb.group({
checkboxes: this.fb.array(this.checkboxes.map(x => false))
});
This will just set the initial value of all the checkboxes to false
.
Next, we need to register these form variables in the template and iterate over the checkboxes
array (NOT the FormArray
but the checkbox data) to display them in the template.
<form [formGroup]="checkboxGroup">
<ng-container *ngFor="let checkbox of checkboxes; let i = index" formArrayName="checkboxes">
<input type="checkbox" [formControlName]="i" />{{checkbox.name}}
</ng-container>
</form>
Make use of the valueChanges observable
Here's the part I don't see mentioned in any answer given here. In situations such as this, where we would like to display said data but store it as something else, the valueChanges
observable is very helpful. Using valueChanges
, we can observe the changes in the checkboxes
and then map
the true
/false
values received from the FormArray
to the desired data. Note that this will not change the selection of the checkboxes as any truthy value passed to the checkbox will mark it as checked and vice-versa.
subscription: Subscription;
const checkboxControl = (this.checkboxGroup.controls.checkboxes as FormArray);
this.subscription = checkboxControl.valueChanges.subscribe(checkbox => {
checkboxControl.setValue(
checkboxControl.value.map((value, i) => value ? this.checkboxes[i].value : false),
{ emitEvent: false }
);
});
This basically maps the FormArray
values to the original checkboxes
array and returns the value
in case the checkbox is marked as true
, else it returns false
. The emitEvent: false
is important here since setting the FormArray
value without it will cause valueChanges
to emit an event creating an endless loop. By setting emitEvent
to false
, we are making sure the valueChanges
observable does not emit when we set the value here.
Filter out the false values
We cannot directly filter the false
values in the FormArray
because doing so will mess up the template since they are bound to the checkboxes. So the best possible solution is to filter out the false
values during submission. Use the spread operator to do this.
submit() {
const checkboxControl = (this.checkboxGroup.controls.checkboxes as FormArray);
const formValue = {
...this.checkboxGroup.value,
checkboxes: checkboxControl.value.filter(value => !!value)
}
// Submit formValue here instead of this.checkboxGroup.value as it contains the filtered data
}
This basically filters out the falsy values from the checkboxes
.
Unsubscribe from valueChanges
Lastly, don't forget to unsubscribe from valueChanges
ngOnDestroy() {
this.subscription.unsubscribe();
}
Note: There is a special case where a value cannot be set to the FormArray
in valueChanges
, i.e if the checkbox value is set to the number 0
. This will make it look like the checkbox cannot be selected since selecting the checkbox will set the FormControl
as the number 0
(a falsy value) and hence keep it unchecked. It would be preferred not to use the number 0
as a value but if it is required, you have to conditionally set 0
to some truthy value, say string '0'
or just plain true
and then on submitting, convert it back to the number 0
.
The StackBlitz also has code for when you want to pass default values to the checkboxes so they get marked as checked in the UI.
TEMPLATE PART:-
<div class="form-group">
<label for="options">Options:</label>
<div *ngFor="let option of options">
<label>
<input type="checkbox"
name="options"
value="{{option.value}}"
[(ngModel)]="option.checked"
/>
{{option.name}}
</label>
</div>
<br/>
<button (click)="getselectedOptions()" >Get Selected Items</button>
</div>
CONTROLLER PART:-
export class Angular2NgFor {
constructor() {
this.options = [
{name:'OptionA', value:'first_opt', checked:true},
{name:'OptionB', value:'second_opt', checked:false},
{name:'OptionC', value:'third_opt', checked:true}
];
this.getselectedOptions = function() {
alert(this.options
.filter(opt => opt.checked)
.map(opt => opt.value));
}
}
}
Add my 5 cents) My question model
{
name: "what_is_it",
options:[
{
label: 'Option name',
value: '1'
},
{
label: 'Option name 2',
value: '2'
}
]
}
template.html
<div class="question" formGroupName="{{ question.name }}">
<div *ngFor="let opt of question.options; index as i" class="question__answer" >
<input
type="checkbox" id="{{question.name}}_{{i}}"
[name]="question.name" class="hidden question__input"
[value]="opt.value"
[formControlName]="opt.label"
>
<label for="{{question.name}}_{{i}}" class="question__label question__label_checkbox">
{{opt.label}}
</label>
</div>
component.ts
onSubmit() {
let formModel = {};
for (let key in this.form.value) {
if (typeof this.form.value[key] !== 'object') {
formModel[key] = this.form.value[key]
} else { //if formgroup item
formModel[key] = '';
for (let k in this.form.value[key]) {
if (this.form.value[key][k])
formModel[key] = formModel[key] + k + ';'; //create string with ';' separators like 'a;b;c'
}
}
}
console.log(formModel)
}
Here's a good place to use the FormArray
https://angular.io/docs/ts/latest/api/forms/index/FormArray-class.html
To start we'll build up our array of controls either with a FormBuilder
or newing up a FormArray
FormBuilder
this.checkboxGroup = _fb.group({
myValues: _fb.array([true, false, true])
});
new FormArray
let checkboxArray = new FormArray([
new FormControl(true),
new FormControl(false),
new FormControl(true)]);
this.checkboxGroup = _fb.group({
myValues: checkboxArray
});
Easy enough to do, but then we're going to change our template and let the templating engine handle how we bind to our controls:
template.html
<form [formGroup]="checkboxGroup">
<input *ngFor="let control of checkboxGroup.controls['myValues'].controls"
type="checkbox" id="checkbox-1" value="value-1" [formControl]="control" />
</form>
Here we're iterating over our set of FormControls
in our myValues
FormArray
and for each control we're binding [formControl]
to that control instead of to the FormArray
control and <div>{{checkboxGroup.controls['myValues'].value}}</div>
produces true,false,true
while also making your template syntax a little less manual.
You can use this example: http://plnkr.co/edit/a9OdMAq2YIwQFo7gixbj?p=preview to poke around
My solution - solved it for Angular 5 with Material View
The connection is through the
formArrayName="notification"
(change)="updateChkbxArray(n.id, $event.checked, 'notification')"
This way it can work for multiple checkboxes arrays in one form. Just set the name of the controls array to connect each time.
constructor(
private fb: FormBuilder,
private http: Http,
private codeTableService: CodeTablesService) {
this.codeTableService.getnotifications().subscribe(response => {
this.notifications = response;
})
...
}
createForm() {
this.form = this.fb.group({
notification: this.fb.array([])...
});
}
ngOnInit() {
this.createForm();
}
updateChkbxArray(id, isChecked, key) {
const chkArray = < FormArray > this.form.get(key);
if (isChecked) {
chkArray.push(new FormControl(id));
} else {
let idx = chkArray.controls.findIndex(x => x.value == id);
chkArray.removeAt(idx);
}
}
<div class="col-md-12">
<section class="checkbox-section text-center" *ngIf="notifications && notifications.length > 0">
<label class="example-margin">Notifications to send:</label>
<p *ngFor="let n of notifications; let i = index" formArrayName="notification">
<mat-checkbox class="checkbox-margin" (change)="updateChkbxArray(n.id, $event.checked, 'notification')" value="n.id">{{n.description}}</mat-checkbox>
</p>
</section>
</div>
At the end you are getting to save the form with array of original records id's to save/update.
Will be happy to have any remarks for improvement.
If you want to use an Angular reactive form (https://angular.io/guide/reactive-forms).
You can use one form control to manage the outputted value of the group of checkboxes.
component
import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
import { flow } from 'lodash';
import { flatMap, filter } from 'lodash/fp';
@Component({
selector: 'multi-checkbox',
templateUrl: './multi-checkbox.layout.html',
})
export class MultiChecboxComponent {
checklistState = [
{
label: 'Frodo Baggins',
value: 'frodo_baggins',
checked: false
},
{
label: 'Samwise Gamgee',
value: 'samwise_gamgee',
checked: true,
},
{
label: 'Merry Brandybuck',
value: 'merry_brandybuck',
checked: false
}
];
form = new FormGroup({
checklist : new FormControl(this.flattenValues(this.checklistState)),
});
checklist = this.form.get('checklist');
onChecklistChange(checked, checkbox) {
checkbox.checked = checked;
this.checklist.setValue(this.flattenValues(this.checklistState));
}
flattenValues(checkboxes) {
const flattenedValues = flow([
filter(checkbox => checkbox.checked),
flatMap(checkbox => checkbox.value )
])(checkboxes)
return flattenedValues.join(',');
}
}
html
<form [formGroup]="form">
<label *ngFor="let checkbox of checklistState" class="checkbox-control">
<input type="checkbox" (change)="onChecklistChange($event.target.checked, checkbox)" [checked]="checkbox.checked" [value]="checkbox.value" /> {{ checkbox.label }}
</label>
</form>
checklistState
Manages the model/state of the checklist inputs. This model allows you to map the current state to whatever value format you need.
Model:
{
label: 'Value 1',
value: 'value_1',
checked: false
},
{
label: 'Samwise Gamgee',
value: 'samwise_gamgee',
checked: true,
},
{
label: 'Merry Brandybuck',
value: 'merry_brandybuck',
checked: false
}
checklist
Form ControlThis control stores the value would like to save as e.g
value output: "value_1,value_2"
See demo at https://stackblitz.com/edit/angular-multi-checklist