Angular ReactiveForms: Producing an array of checkbox values?
Asked Answered
A

19

154

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', ...]
Amigo answered 2/12, 2016 at 7:44 Comment(9)
This is probably the most over-engineered way to do checkboxes in a form ever. This is not straightforward at all.Effluent
@Effluent are you specifically complaining about my implementation or Angular?Amigo
Angular. All I'm trying to do is get a mat-radio-group to bind in my reactive form. I don't remember struggling this much with angular. All the articles are pointing to the same thing. Just can't get it to work. Everything else is super straightforward. I've probably been looking at it way too long. Still just feels like wayyyyy too much complexity for an array value in a form.Effluent
Yeah it was terrible when I asked this in 2016, and it's still terrible in 2019.Amigo
I'm not adding a ton to this question, but I wanted it known to others that I feel the same way. This alone has been the hardest part in learning angular reactive forms. I feel like it shouldn't be this difficult, at all. I'm happy to see I'm not alone in the struggle, though. So thanks for posting the question.Futhark
this is crazy, and all the solutions are complex for gathering simple checked values, this is a good example of big framework making simple stuff more complex. my stupid simple solution is to have a hidden ng input for the actual question, and use basic html (no ng bindings)+js to render and handle onchange event, which then gathers and puts data I want in hidden ng input for ng to pick up.Capitoline
ReactiveForms and dealing with groups of checkboxes and radio buttons is pretty much the single most horrible thing about Angular. I love the type safety and OOP structure but this alone is enough of a head ache for me to consider alternatives.Pyrope
Hey @MartinSotirov, I completely agree with you! It's insane how Angular still doesn't have a simple way to solve this. After a lot of research and test, I was able to come up with a elegant solution, you might want to check and see what you think: https://mcmap.net/q/156309/-angular-reactiveforms-producing-an-array-of-checkbox-valuesAnnelieseannelise
looking at your code, i found out my own problem that was a totally different thing :) thanks <3Stringendo
D
87

With the help of silentsod answer, I wrote a solution to get values instead of states in my formBuilder.

I use a method to add or remove values in the formArray. It may be a bad approch, but it works !

component.html

<div *ngFor="let choice of checks; let i=index" class="col-md-2">
  <label>
    <input type="checkbox" [value]="choice.value" (change)="onCheckChange($event)">
    {{choice.description}}
  </label>
</div>

component.ts

// For example, an array of choices
public checks: Array<ChoiceClass> = [
  {description: 'descr1', value: 'value1'},
  {description: "descr2", value: 'value2'},
  {description: "descr3", value: 'value3'}
];

initModelForm(): FormGroup{
  return this._fb.group({
    otherControls: [''],
    // The formArray, empty 
    myChoices: new FormArray([]),
  }
}

onCheckChange(event) {
  const formArray: FormArray = this.myForm.get('myChoices') as FormArray;

  /* Selected */
  if(event.target.checked){
    // Add a new control in the arrayForm
    formArray.push(new FormControl(event.target.value));
  }
  /* unselected */
  else{
    // find the unselected element
    let i: number = 0;

    formArray.controls.forEach((ctrl: FormControl) => {
      if(ctrl.value == event.target.value) {
        // Remove the unselected element from the arrayForm
        formArray.removeAt(i);
        return;
      }

      i++;
    });
  }
}

When I submit my form, for example my model looks like:

  otherControls : "foo",
  myChoices : ['value1', 'value2']

Only one thing is missing, a function to fill the formArray if your model already has checked values.

Del answered 3/5, 2017 at 22:50 Comment(6)
how do i check if my checkbox is checked when i load the data after using your example to enter into db?Perversity
In this solution always the form is being valid even if checkbox aren't selectedAmortizement
myChoices: new FormArray([], Validators.required)Vegetate
"But it works!" is how tech debt starts. This is not the reactive form way of doing it. One of the benefits of using formcontrols on every checkbox input is they can remember their states even when you re-add them to the DOM.Boren
Hey, I designed an extensible and object-oriented solution to solve this use case, you can check my answer here: https://mcmap.net/q/156309/-angular-reactiveforms-producing-an-array-of-checkbox-valuesAnnelieseannelise
@Boren That's nice and all, but the built-in FormArray has no obvious way to keep track of the checkbox labels. You're basically stuck with keeping track of it yourself.Treytri
N
60

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

Nemertean answered 2/12, 2016 at 17:27 Comment(8)
probably you should remove id="xxx", the id should be unique right?Jacquettajacquette
for id could be used index *ngFor="let control of checkboxGroup.controls['myValues'].controls ; let i=index""Culicid
This is cool, but produces a completely generic array of checkboxes. Presumably you would be loading in an array or something else, and associating each checkbox with some other value. How do you add, for instance, a text string for use in a form label to each form control?Resnatron
NM I've just mapped it next to an external array :pResnatron
@Resnatron can you post your solution to have the checkboxes and the labels?Armidaarmiger
@Armidaarmiger It's too intertwined in the app code (which is not public), but basically what I did was: 1) read in the data from the api (an array of things) 2) use a spread, for or map, to make 2 new arrays. One array is used to generate the checkboxes, and the other is for the label text. They just have the same indexes. 3) ngFor over each array seperately in the UI. One loop to make the labels, the other to make the checkboxes.Resnatron
@Armidaarmiger I've created two fields in group (each group in created array). One field is boolean and the other one is string (my label). In HTML (in each [formGroup]): <label>{{groupItem.controls.languageLabel.value}}</label>Hogarth
Tried in my project, still get [true, false..] ...Am I missing something? Or does it still work in latest Angular?Dre
N
42

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>
Namely answered 17/9, 2018 at 15:14 Comment(9)
Very usable as well in case of a simple hardcoded array of checkbox values. Then you can add the form controls using a similar for loop right away in ngOnInit(), and the checkboxes in your form will dynamically reflect the checkbox values arrayOstracize
This still excerpts [key1=true,key2=false,key3=true]. We want ['key1','key3']Reservist
@Reservist You can do it like that: ` const value = { key1: true, key2: false, key3: true }; const list = Object.entries(value).filter(([_, isSelected]) => isSelected).map(([key]) => key); console.log(list); `Saxton
Best solution imho. You can place the assignment of const checkboxes = .. outside the foreach ;)Unsought
What happens when the item key equals the same thing as another field on the form? For instance, I have two different checkbox arrays, each with the keys 'Small', 'Medium', and 'Large'?Grippe
@Grippe On your form, use addControl() to add a formGroup for each checkbox array, naming them differentlyLsd
I see. I guess it appeared to me that the [formControlName] binding would have no way to associate to the correct name if the binding is in a loop. How can two different loops with [formControlName]="item.key" bind to the correct control if the keys happen to be the same? What am I missing? Adding two form groups with different names means those names need to be the same as the controls' formControlName, right?Grippe
This solution looks nice and clean, but I'm running into an error: Type 'unknown' is not assignable to type 'number'. <input type="checkbox" [formControlName]="item.key" [value]="item.value" /> {{ item.key }}Fernyak
How would I store a label to go along with this? The only way I can see to do this is to manage two separate arrays but I'd like to avoid that if possible.Leatherleaf
G
20

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.

Galactopoietic answered 6/10, 2019 at 22:51 Comment(2)
This still requires maintaining two arrays and keeping them in sync. Still not as clean as I'm hoping for. Perhaps we can get the form controls to hold complex values instead of having two arrays.Boren
Complex value didn't work because the value has to be either true or false for the checkbox. So this solution still looks the best.Boren
M
16

TL;DR

  1. I prefer to use FormGroup to populate the list of checkbox
  2. Write a custom validator to 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 sometimes so I did try both FormArray and FormGroup approaches.

Most of the time, the list of checkboxes was populated on the server and I received it through API. But sometimes you will have a static set of checkboxes 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 categories to select from, required at least one to be checked. Assume the list will be retrieved from the server.

List of category

First, I set up a form with only the 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 takes a list of selected values as an argument so If you want to reuse the form for editing data, it could be helpful. For the purpose of creating a new product form, it is not 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

formGroup will store the form data as the object, which required a key map to a form control. So it is a 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 this:

{
    "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 compared 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 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) {
        checked++;
      }
    });

    if (checked < minRequired) {
      return {
        requireCheckboxToBeChecked: true,
      };
    }

    return null;
  };
}
Monospermous answered 17/6, 2019 at 13:49 Comment(0)
B
11

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>
Bobbybobbye answered 4/8, 2017 at 8:41 Comment(1)
You guys need to copy the pipe mapToKeys from example link or as below if that link dies. You may need to remove the second param (args) and declare the pipe in app module file: import {Pipe, PipeTransform} from '@angular/core'; @Pipe({name: 'mapToKeys'}) export class MapToKeysPipe implements PipeTransform { transform(value) : any { let keys = []; for (let key in value) { keys.push({key: key, value: value[key]}); } return keys; } }Kierakieran
R
7

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

Reichert answered 23/11, 2018 at 15:57 Comment(1)
Easily the best solution for me. Thank you so much.Grippe
A
4

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.

Aeschines answered 16/5, 2017 at 17:41 Comment(1)
This got me on the right track, I ended up doing this this.customForm.patchValue({[event.target.id] : event.target.checked});Worshipful
A
3

Apparently, this is a very common problem and no one has a "perfect" solution. I believe I was able to come with a pretty elegant solution, using object orientation to extend the capabilities of FormGroup.

Desired API

In a single object I want to be able to have:

  • The form control for each checkbox
  • The label and value for each checkbox
  • The values of all selected checkbox

So the HTML structure can be simple as this:

<div *ngFor="let item of checkboxGroup.items">
   <input type="checkbox" [id]="item.value" [formControl]="item.control">
   <label [for]="item.value">{{ item.label }}</label>
 </div>

And the typescript part can be simple as this:

checkboxGroup.value; // return the list of selected item values
checkboxGroup.control.valid; // return if there's at least one checked value

Solution

As you can see in the HTML part, the checkboxGroup needs to be a class with at least three properties:

  • items (each item is a checkbox with a value, label and a FormControl)
  • value (gets all selected items)
  • control (gets the FormArray control)

So the class will be like:

// # This represents a single checkbox item
class CheckboxItemControl {
  label: string; // value to be shown in the UI
  value: string; // value to be saved in backend

  control: FormControl;

  constructor({ label, value, defaultValue = false }: { label: string; value: string; defaultValue?: boolean }) {
    this.label = label;
    this.value = value;

    this.control = new FormControl(defaultValue || false);
  }

  get selected(): boolean {
    return Boolean(this.control.value);
  }
}

// # This represents a checkbox group, with several items
class CheckboxGroupControl {
  name?: string; // name of the checkbox group

  items: CheckboxItemControl[];
  control: FormArray;

  constructor(name: string, items: CheckboxItemControl[]) {
    this.name = name;
    this.items = items;

    this.control = new FormArray(this.getAllItemsControls(), CheckboxGroupControl.emptyArrayFormValidator);
  }

  get value(): string[] {
    return this.selectedItems.map(item => item.value);
  }

  private get selectedItems(): CheckboxItemControl[] {
    return this.items.filter(item => item.selected);
  }

  private getAllItemsControls(): FormControl[] {
    return this.items.map(item => item.control);
  }

  private static emptyArrayFormValidator(control: FormControl) {
    const valid = (control.value as boolean[]).some(Boolean);

    // @todo improve error message
    return valid ? null : {
      error: 'empty'
    };
  }
}

You can see how each class exposes a simple API (object.value and object.control) which allows you to easily get all that you need.

Usage

So let's see in practice how it'll work:

HTML

<div *ngFor="let item of checkboxGroup.items">
   <input type="checkbox" [id]="item.value" [formControl]="item.control">
   <label [for]="item.value">{{ item.label }}</label>
 </div>

Typescript

checkboxGroup;

ngOnInit() {
  this.createFormInputs();
}

private createFormInputs() {
  const checkboxItems = [
    new CheckboxItemControl({ value: 'checkbox-1', label: 'Checkbox 1' }),
    new CheckboxItemControl({ value: 'checkbox-2', label: 'Checkbox 2' }),
    new CheckboxItemControl({ value: 'checkbox-3', label: 'Checkbox 3', defaultValue: true })
  ];

  this.checkboxGroup = new CheckboxGroupControl('name_of_group', checkboxItems);

  this.form = new FormGroup({
    checkbox: this.checkboxGroup.control
  });

  // this.checkboxGroup.value returns ['checkbox-1', ...] for the selected checkboxes
  // this.checkboxGroup.valid returns if there's any checkbox selected
  // this.form.valid returns if the whole form is valid. Which is useful if you include others checkbox groups
}

Other resources

  • This article sparks the light for me to come up with this solution.
Annelieseannelise answered 19/10, 2021 at 20:55 Comment(0)
A
2

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. The UI View

The relevat part of the json of the form

Will be happy to have any remarks for improvement.

Apotheosis answered 23/5, 2018 at 11:58 Comment(1)
It's worth noting that the Select with multiple flag in Material accomplishes this out of box material.angular.io/components/select/examplesKnocker
M
1

Component:

formGroup: FormGroup;

games = [
  { keyword: 'hots', score: 9 },
  { keyword: 'xcom', score: 9 },
  { keyword: 'fallout', score: 8 }
];

constructor(private fb: FormBuilder) {}

ngOnInit() {
  this.formGroup = this.fb.group(
    this.games.reduce((obj, game) => {
      obj[game.keyword] = [false];
      return obj;
    }, {})
  );

  const enabledGames$ = this.formGroup.valueChanges.pipe(
    map(value =>
      Object.entries(value)
        .filter(([key, enabled]) => enabled)
        .map(([key]) => 
          this.games.find(({ keyword }) => keyword === key)
        )
    )
  );
}

Template:

<form [formGroup]="formGroup">
  <div *ngFor="let control of formGroup.controls | keyvalue">
    <input
      type="checkbox"
      [formControlName]="control.key">
    <label>
      {{ control.key }}
    </label>
  </div>
</form>
Manchu answered 26/8, 2021 at 11:23 Comment(0)
D
1

With two way binding

my.component.html

<form [formGroup]="formGroup" (ngSubmit)="onSubmit()">

  <div formGroupName="options">
    <mat-checkbox formControlName="myVal1">My Value 1</mat-checkbox>
    <mat-checkbox formControlName="myVal2">My Value 2</mat-checkbox>
  </div>


  <button type="submit">Submit</button>

</form>

my.component.ts

export class ClientStatementReportComponent implements OnInit {

  formGroup: FormGroup;

  ngOnInit(): void {

    this.formGroup = new FormGroup({
      options: new FormGroup({
        myVal1: new FormControl(false),
        myVal2: new FormControl(false)
      }),
    });
  }

  onSubmit() {
    const options = this.formGroup.value.options;
    const result = Object.keys(options).filter(key => options[key])
    // is array of checked elements e.g. ["myVal1"]
  }
}

With one way binding (to form state)

my.component.html

<form [formGroup]="formGroup">

  <mat-checkbox value="val-1" (change)="selectOption($event)">Value 1</mat-checkbox>
  <mat-checkbox value="val-2" (change)="selectOption($event)">Value 2</mat-checkbox>
 
</form>

my.component.ts

export class MyComponent implements OnInit {

  formGroup: FormGroup;

  ngOnInit(): void {
    this.formGroup = new FormGroup({
      options: new FormControl([]),
    });
  }

  selectOption($event: MatCheckboxChange) {
    const value = $event.source.value;
    const optionControl = this.formGroup.controls['options']
    const options = optionControl.value as [];

    if(checked){
      optionControl.setValue([...options, value])
    } else {
      optionControl.setValue(options.filter(option => option !== value))
    }
  }   
}
Dhaulagiri answered 17/12, 2021 at 17:25 Comment(2)
Please let me know how I can improve the answer :)Dhaulagiri
Works well. A little fix for one-way binding though: if($event.checked){Fleischer
G
0

Here's a set of directives that can accomplish this easily, with nothing more than a standard FormGroup, and minimal effect to your template:

StackBlitz Demo Here

Inside your component, define your FormGroup:

formGroup = new FormGroup({
  resultArray: new FormControl([])
})

Inside your template, set your formControlName on the div wrapping all of your checkboxes, with the new checkboxArray directive as well.

On each checkbox, set checkboxArrayKey to the value you want to include if this checkbox is selected

<form [formGroup]="formGroup">
  <!-- ... -->
  <div checkboxArray formControlName="resultArray">
    <div *ngFor="let option of checkboxOptions">
      <input type="checkbox" [checkboxArrayKey]="option.value">
      <label>{{option.label}}</label>
    </div>
  </div>
  <!-- ... -->
</form>

Your form control will now have a value reflecting only the checkboxes that are selected

The directive itself is a little rough and could probably use some cleanup:

import { AfterContentInit, ContentChildren, Directive, ElementRef, forwardRef, Input, OnDestroy, Output, QueryList, Renderer2 } from "@angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
import { merge, of, Subject, takeUntil } from "rxjs";

@Directive({
    selector: 'input[type=checkbox][checkboxArrayKey]',
    host: {
        '(change)': 'onChange($event.target.checked)', 
        '(blur)': 'onTouched()'
    }
})
export class CheckboxArrayKey {
    @Input("checkboxArrayKey") key: any;
    parent?: CheckboxArrayValueAccessor;
    state: boolean = false;

    constructor(private renderer: Renderer2, private element: ElementRef) {
    }

    writeValue(value: boolean) {
        this.state = value;
        this.renderer.setProperty(this.element.nativeElement, "checked", value);
    }

    onChange(value: boolean) {
        this.state = value;
        this.parent?.onChange();
    }

    onTouched() {
        this.parent?.onTouched();
    }
    
    setDisabledState(isDisabled: boolean) {
        this.renderer.setProperty(this.element.nativeElement, "disabled", isDisabled)
    }
}


export const CHECKBOX_ARRAY_VALUE_ACCESSOR: any = {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CheckboxArrayValueAccessor),
    multi: true
}


@Directive({
    selector: '[checkboxArray][formControlName], [checkboxArray][formControl], [checkboxArray][ngModel]',
    providers: [CHECKBOX_ARRAY_VALUE_ACCESSOR]
})
export class CheckboxArrayValueAccessor implements ControlValueAccessor, AfterContentInit, OnDestroy {
    @ContentChildren(CheckboxArrayKey, {descendants: true}) checkboxes?: QueryList<CheckboxArrayKey>;

    private state: any[] = [];
    private destroy = new Subject<void>();
    private onWriteValue = new Subject<void>();
    private _onChange = (_: any) => {};
    private _onTouched = () => {};

    ngAfterContentInit() {
        // Checkboxes will be defined by now - the if is a hack to resolve the potential undefined state
        if (this.checkboxes) {
            merge(of(0), this.checkboxes.changes, this.onWriteValue).pipe(
                takeUntil(this.destroy),
            ).subscribe({ next: () => {
                this.linkChildren();
            }});
        }
    }

    private linkChildren() {
        var me = this;
        var lastState = this.state;

        this.checkboxes?.forEach(chk => { 
            chk.writeValue(lastState.indexOf(chk.key) > -1); 
            chk.parent = me;
        });

        //Hack to avoid changes after a change detection cycle
        Promise.resolve(null).then(() => this.onChange());
    }

    writeValue(obj: any): void {
        if (Array.isArray(obj)) {
            this.state = obj;
            this.onWriteValue.next();
        }
    }

    registerOnChange(fn: (_: any) => void): void {
        this._onChange = fn;
    }

    registerOnTouched(fn: () => void): void {
        this._onTouched = fn;
    }

    setDisabledState(isDisabled: boolean): void {
        this.checkboxes?.forEach(chk => chk.setDisabledState(isDisabled));
    }

    onChange() {
        this._onChange(this.checkboxes?.filter(chk => chk.state).map(chk => chk.key) || []);
    }

    onTouched() {
        this._onTouched();
    }

    ngOnDestroy(): void {
        this.destroy.next();
        this.destroy.complete();
        this.onWriteValue.complete();
    }
}

But it makes for extremely versatile and unobtrusive implementations.

Glomerulonephritis answered 6/4, 2023 at 7:28 Comment(1)
Works quite well when setting up a new form but if the form already has an existing value its not populating the values during linkChildren. Solved this by adding... @Input() public formControl: FormControl|null = null; lastState = (this.formControl?.value!).toString().split(',');Forecast
F
0

Sorry for my late!

I don't know if is the best implementation, but works fine to me. Works with Angular Material too. For me, is simple to read and undestand.

In the example I set and FormBuilder value. Inside a <form [formGroup]="formData">

form-body.component.html

<section>
  <label for="pepperoni">Pepperoni</label>
  <input id="pepperoni" type="checkbox" (change)="addValue($event)" value="pepperoni">
  <label for="extracheese">Extra Cheese</label>
  <input id="extracheese" type="checkbox" (change)="addValue($event)" value="extracheese">
  <label for="mushroom">Mushroom</label>
  <input id="mushroom" type="checkbox" (change)="addValue($event)" value="mushroom">
</section>

form-boby.component.ts

constructor(private formBuilder: FormBuilder) {
    this.formData = this.formBuilder.group({
    toppings: ['']
   });
}

addValue($event: any) {
const value = $event.target.value;
const checked = $event.target.checked;
const checkValues: string[] = this.formData.get('toppings')?.value || []

if (checked) {
  checkValues.push(value);
} else {
  const index = checkValues.indexOf(value)
  checkValues.splice(index, 1)
}
this.formData.get('toppings')?.setValue(checkValues)
console.log(this.formData.get('toppings')?.value)
}

For use with Angular Material, you must need to change "target" in function to "source".

I hope it is useful for someone!

Frau answered 5/3 at 22:10 Comment(0)
U
-1

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)
   }
Uncovenanted answered 26/8, 2017 at 19:49 Comment(0)
R
-1

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);
Rodrigorodrigue answered 24/2, 2021 at 20:56 Comment(0)
B
-1

Template

 <div>
    <input name="fruits" type="checkbox" value="orange" (change)="change($event)">
    <input name="fruits" type="checkbox" value="apple" (change)="change($event)">
    <input name="fruits" type="checkbox" value="banana" (change)="change($event)">
</div>

Component


    formGroup = this.formBuilder.group(
      {
        fruits: [[]]  //["Orange","Banana",...]
      })

    change(event: Event) {
      let target = (event.target as HTMLInputElement);
      let array = (this.formGroup.get(target.name)?.value as Array);
    
      if (target.checked && !array.find(element => {
        return (element === target.value);
      })) {
        array.push(target.value)// element not exists, push (check)
      }
      else {
        array.splice(array.findIndex(element => {
          return (element === target.value);//delete element (uncheck)
        }), 1)
      }
    }

This is how I would do it, although I always use Angular Material List

https://material.angular.io/components/list/overview

Everything comes from the factory for these tasks

Bazemore answered 30/11, 2021 at 14:15 Comment(0)
C
-1

Below code can also help you render dynamically received data from API call.

In your component class file: (Data from API call)

this.adminService.getSystemConfigs().subscribe(configs => {
    this.systemConfigs = configs;

    this.createForm();
    this.getUserConfigs();
});

getUserConfigs(): void {
    this.adminService.getUserConfigs().subscribe(configs => {
        this.userConfigs = configs;
        this.patchForm();
    });
}

In your component class file: (Data is hardcoded)

this.systemConfigs = [
    {
        id: '123',
        key: 'Is allowed to edit',
    },
    {
        id: '456',
        key: 'Is allowed to publish',
    }
];

this.userConfigs = [
    {
        id: '456',
        key: 'Is allowed to publish',
    }
];

get configsArr() {
    return this.userForm.get('configs') as FormArray;
}

createForm(): void {
    this.userForm = this.formBuilder.group({
        ...Your other form fields,
        configs: this.formBuilder.array(this.systemConfigs.map(_ => this.formBuilder.control(false))),
    });
}

patchForm(): void {
    // Patch config values
    this.systemConfigs.map((sysConfig, i) => {
        // Get list of user config ID's to setvalues for checkbox
        const userConfigIds = this.userConfigs.map(userConfig => userConfig.id);
        if (userConfigIds.includes(sysConfig.id)) {
            this.configsArr.at(i).patchValue(true);
        }
    });
}

onSubmit(): void {
    const selectedConfig = this.userForm.value.configs
        .map((checked, i) => ({ id: checked ? this.systemConfigs[i].id : null }))
        .filter(value => value.id !== null);
    // selectedConfig can now be send as payload to API call.
}

Your HTML will look like this.

<label class="form-label option" formArrayName="configs" *ngFor="let configControl of configsArr.controls; let i = index">
    <input type="checkbox" [formControlName]="i" />
        {{ systemConfigs[i].key }}
</label>

Demo Here

Candless answered 19/10, 2023 at 16:18 Comment(0)
T
-2

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

        }
Towne answered 2/12, 2016 at 10:58 Comment(2)
Hi @EchoLogic .. Please let me know in case of any queryTowne
This is not using ReactiveForms but regular forms so doesn't answer the questionErny

© 2022 - 2024 — McMap. All rights reserved.