[javascript] Angular ReactiveForms: Producing an array of checkbox values?

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 simply true/false?

Example:

<form [formGroup]="checkboxGroup">
    <input type="checkbox" id="checkbox-1" value="value-1" formControlName="myValues" />
    <input type="checkbox" id="checkbox-2" value="value-2" formControlName="myValues" />
    <input type="checkbox" id="checkbox-3" value="value-2" formControlName="myValues" />
</form>

checkboxGroup.controls['myValues'].value currently produces:

true or false

What I want it to produce:

['value-1', 'value-2', ...]

This question is related to javascript angular checkbox angular2-forms

The answer is


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


If you are looking for checkbox values in JSON format

{ "name": "", "countries": [ { "US": true }, { "Germany": true }, { "France": true } ] }

Full example here.

I apologise for using Country Names as checkbox values instead of those in the question. Further explannation -

Create a FormGroup for the form

 createForm() {

    //Form Group for a Hero Form
    this.heroForm = this.fb.group({
      name: '',
      countries: this.fb.array([])
    });

    let countries=['US','Germany','France'];

    this.setCountries(countries);}
 }

Let each checkbox be a FormGroup built from an object whose only property is the checkbox's value.

 setCountries(countries:string[]) {

    //One Form Group for one country
    const countriesFGs = countries.map(country =>{
            let obj={};obj[country]=true;
            return this.fb.group(obj)
    });

    const countryFormArray = this.fb.array(countriesFGs);
    this.heroForm.setControl('countries', countryFormArray);
  }

The array of FormGroups for the checkboxes is used to set the control for the 'countries' in the parent Form.

  get countries(): FormArray {
      return this.heroForm.get('countries') as FormArray;
  };

In the template, use a pipe to get the name for the checkbox control

  <div formArrayName="countries" class="well well-lg">
      <div *ngFor="let country of countries.controls; let i=index" [formGroupName]="i" >
          <div *ngFor="let key of country.controls | mapToKeys" >
              <input type="checkbox" formControlName="{{key.key}}">{{key.key}}
          </div>
      </div>
  </div>

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)
   }

I was able to accomplish this using a FormArray of FormGroups. The FormGroup consists of two controls. One for the data and one to store the checked boolean.

TS

options: options[] = [{id: 1, text: option1}, {id: 2, text: option2}];

this.fb.group({
  options: this.fb.array([])
})    

populateFormArray() {    
  this.options.forEach(option => {                       
    let checked = ***is checked logic here***;            
    this.checkboxGroup.get('options').push(this.createOptionGroup(option, checked))
  });       
}  

createOptionGroup(option: Option, checked: boolean) {
  return this.fb.group({      
    option: this.fb.control(option),
    checked: this.fb.control(checked)
  });
}

HTML

This allows you to loop through the options and bind to the corresponding checked control.

<form [formGroup]="checkboxGroup">
  <div formArrayName="options" *ngFor="let option of options; index as i">   
    <div [formGroupName]="i">
      <input type="checkbox" formControlName="checked" />
      {{ option.text }}
    </div>
  </div>       
</form>

Output

The form returns data in the form {option: Option, checked: boolean}[].

You can get a list of checked options using the below code

 this.checkboxGroup.get('options').value.filter(el => el.checked).map(el => el.option);

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.

_x000D_
_x000D_
constructor(_x000D_
  private fb: FormBuilder,_x000D_
  private http: Http,_x000D_
  private codeTableService: CodeTablesService) {_x000D_
_x000D_
  this.codeTableService.getnotifications().subscribe(response => {_x000D_
      this.notifications = response;_x000D_
    })_x000D_
    ..._x000D_
}_x000D_
_x000D_
_x000D_
createForm() {_x000D_
  this.form = this.fb.group({_x000D_
    notification: this.fb.array([])..._x000D_
  });_x000D_
}_x000D_
_x000D_
ngOnInit() {_x000D_
  this.createForm();_x000D_
}_x000D_
_x000D_
updateChkbxArray(id, isChecked, key) {_x000D_
  const chkArray = < FormArray > this.form.get(key);_x000D_
  if (isChecked) {_x000D_
    chkArray.push(new FormControl(id));_x000D_
  } else {_x000D_
    let idx = chkArray.controls.findIndex(x => x.value == id);_x000D_
    chkArray.removeAt(idx);_x000D_
  }_x000D_
}
_x000D_
<div class="col-md-12">_x000D_
  <section class="checkbox-section text-center" *ngIf="notifications  && notifications.length > 0">_x000D_
    <label class="example-margin">Notifications to send:</label>_x000D_
    <p *ngFor="let n of notifications; let i = index" formArrayName="notification">_x000D_
      <mat-checkbox class="checkbox-margin" (change)="updateChkbxArray(n.id, $event.checked, 'notification')" value="n.id">{{n.description}}</mat-checkbox>_x000D_
    </p>_x000D_
  </section>_x000D_
</div>
_x000D_
_x000D_
_x000D_

At the end you are getting to save the form with array of original records id's to save/update. The UI View

The relevat part of the json of the form

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 Control

This 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


Related answer to @nash11, here's how you would produce an array of checkbox values

AND

have a checkbox that also selectsAll the checkboxes:

https://stackblitz.com/edit/angular-checkbox-custom-value-with-selectall


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.


Summary

Here's the pith of the detailed explanation along with a StackBlitz example.

  1. Use FormArray for the checkboxes and initialize the form.
  2. The 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.
  3. Filter out the false values at the time of submission.
  4. Unsubscribe from valueChanges observable.

StackBlitz example


Detailed explanation

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.

StackBlitz example

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));
                }
             }

        }

It's significantly easier to do this in Angular 6 than it was in previous versions, even when the checkbox information is populated asynchronously from an API.

The first thing to realise is that thanks to Angular 6's keyvalue pipe we don't need to have to use FormArray anymore, and can instead nest a FormGroup.

First, pass FormBuilder into the constructor

constructor(
    private _formBuilder: FormBuilder,
) { }

Then initialise our form.

ngOnInit() {

    this.form = this._formBuilder.group({
        'checkboxes': this._formBuilder.group({}),
    });

}

When our checkbox options data is available, iterate it and we can push it directly into the nested FormGroup as a named FormControl, without having to rely on number indexed lookup arrays.

const checkboxes = <FormGroup>this.form.get('checkboxes');
options.forEach((option: any) => {
    checkboxes.addControl(option.title, new FormControl(true));
});

Finally, in the template we just need to iterate the keyvalue of the checkboxes: no additional let index = i, and the checkboxes will automatically be in alphabetical order: much cleaner.

<form [formGroup]="form">

    <h3>Options</h3>

    <div formGroupName="checkboxes">

        <ul>
            <li *ngFor="let item of form.get('checkboxes').value | keyvalue">
                <label>
                    <input type="checkbox" [formControlName]="item.key" [value]="item.value" /> {{ item.key }}
                </label>
            </li>
        </ul>

    </div>

</form>

Make an event when it's clicked and then manually change the value of true to the name of what the check box represents, then the name or true will evaluate the same and you can get all the values instead of a list of true/false. Ex:

component.html

<form [formGroup]="customForm" (ngSubmit)="onSubmit()">
    <div class="form-group" *ngFor="let parameter of parameters"> <!--I iterate here to list all my checkboxes -->
        <label class="control-label" for="{{parameter.Title}}"> {{parameter.Title}} </label>
            <div class="checkbox">
              <input
                  type="checkbox"
                  id="{{parameter.Title}}"
                  formControlName="{{parameter.Title}}"
                  (change)="onCheckboxChange($event)"
                  > <!-- ^^THIS^^ is the important part -->
             </div>
      </div>
 </form>

component.ts

onCheckboxChange(event) {
    //We want to get back what the name of the checkbox represents, so I'm intercepting the event and
    //manually changing the value from true to the name of what is being checked.

    //check if the value is true first, if it is then change it to the name of the value
    //this way when it's set to false it will skip over this and make it false, thus unchecking
    //the box
    if(this.customForm.get(event.target.id).value) {
        this.customForm.patchValue({[event.target.id] : event.target.id}); //make sure to have the square brackets
    }
}

This catches the event after it was already changed to true or false by Angular Forms, if it's true I change the name to the name of what the checkbox represents, which if needed will also evaluate to true if it's being checked for true/false as well.


TL;DR

  1. I prefer to use FormGroup to populate the list of checkbox
  2. Write a custom validator for check at least one checkbox was selected
  3. Working example https://stackblitz.com/edit/angular-validate-at-least-one-checkbox-was-selected

This also struck me for sometimes so I did try both FormArray and FormGroup approaches.

Most of the time, the list of checkbox was populated on the server and I received it through API. But sometimes you will have a static set of checkbox with your predefined value. With each use case, the corresponding FormArray or FormGroup will be used.

Basically FormArray is a variant of FormGroup. The key difference is that its data gets serialized as an array (as opposed to being serialized as an object in case of FormGroup). This might be especially useful when you don’t know how many controls will be present within the group, like dynamic forms.

For the sake of simplicity, imagine you have a simple create product form with

  • One required product name textbox.
  • A list of category to select from, required at least one to be checked. Assume the list will be retrieved from the server.

First, I set up a form with only product name formControl. It is a required field.

this.form = this.formBuilder.group({
    name: ["", Validators.required]
});

Since the category is dynamically rendering, I will have to add these data into the form later after the data was ready.

this.getCategories().subscribe(categories => {
    this.form.addControl("categoriesFormArr", this.buildCategoryFormArr(categories));
    this.form.addControl("categoriesFormGroup", this.buildCategoryFormGroup(categories));
})

There are two approaches to build up the category list.

1. Form Array

  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>

This buildCategoryFormGroup will return me a FormArray. It also take a list of selected value as an argument so If you want to reuse the form for edit data, it could be helpful. For the purpose of create a new product form, it is not be applicable yet.

Noted that when you try to access the formArray values. It will looks like [false, true, true]. To get a list of selected id, it required a bit more work to check from the list but based on the array index. Doesn't sound good to me but it works.

get categoriesFormArraySelectedIds(): string[] {
  return this.categories
  .filter((cat, catIdx) => this.categoriesFormArr.controls.some((control, controlIdx) => catIdx === controlIdx && control.value))
  .map(cat => cat.id);
}

That's why I came up using FormGroup for that matter

2. Form Group

The different of the formGroup is it will store the form data as the object, which required a key and a form control. So it is the good idea to set the key as the categoryId and then we can retrieve it later.

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>

The value of the form group will look like:

{
    "category1": false,
    "category2": true,
    "category3": true,
}

But most often we want to get only the list of categoryIds as ["category2", "category3"]. I also have to write a get to take these data. I like this approach better comparing to the formArray, because I could actually take the value from the form itself.

  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. Custom validator to check at least one checkbox was selected

I made the validator to check at least X checkbox was selected, by default it will check against one checkbox only.

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;
  };
}

Examples related to javascript

need to add a class to an element How to make a variable accessible outside a function? Hide Signs that Meteor.js was Used How to create a showdown.js markdown extension Please help me convert this script to a simple image slider Highlight Anchor Links when user manually scrolls? Summing radio input values How to execute an action before close metro app WinJS javascript, for loop defines a dynamic variable name Getting all files in directory with ajax

Examples related to angular

error NG6002: Appears in the NgModule.imports of AppModule, but could not be resolved to an NgModule class error TS1086: An accessor cannot be declared in an ambient context in Angular 9 TS1086: An accessor cannot be declared in ambient context @angular/material/index.d.ts' is not a module Why powershell does not run Angular commands? error: This is probably not a problem with npm. There is likely additional logging output above Angular @ViewChild() error: Expected 2 arguments, but got 1 Schema validation failed with the following errors: Data path ".builders['app-shell']" should have required property 'class' Access blocked by CORS policy: Response to preflight request doesn't pass access control check origin 'http://localhost:4200' has been blocked by CORS policy in Angular7

Examples related to checkbox

Setting default checkbox value in Objective-C? Checkbox angular material checked by default Customize Bootstrap checkboxes Angular ReactiveForms: Producing an array of checkbox values? JQuery: if div is visible Angular 2 Checkbox Two Way Data Binding Launch an event when checking a checkbox in Angular2 Checkbox value true/false Angular 2: Get Values of Multiple Checked Checkboxes How to change the background color on a input checkbox with css?

Examples related to angular2-forms

How can I manually set an Angular form field as invalid? Angular 2 Cannot find control with unspecified name attribute on formArrays Remove all items from a FormArray in Angular Angular ReactiveForms: Producing an array of checkbox values? How to make readonly all inputs in some div in Angular2? tslint / codelyzer / ng lint error: "for (... in ...) statements must be filtered with an if statement" Min / Max Validator in Angular 2 Final TypeScript-'s Angular Framework Error - "There is no directive with exportAs set to ngForm" Can't bind to 'formGroup' since it isn't a known property of 'form' ngModel cannot be used to register form controls with a parent formGroup directive