2
votes

I'm trying to create a form which displays dynamic checkboxes, at least one of which should be checked in order to proceed. I also need to get an array of checked checkboxes.

Here's the code from the component:

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { FormControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-products',
  templateUrl: './products.component.html',
  styleUrls: ['./products.component.scss']
})
export class ProductsComponent implements OnInit {
  productsForm: FormGroup;
  items = [
    {key: 'key1', text: 'text1'},
    {key: 'key2', text: 'text2'},
    {key: 'key3', text: 'text3'},
  ];
  checkboxGroup = new FormArray(this.items.map(item => new FormGroup({
    id: new FormControl(item.key),
    text: new FormControl(item.text),
    checkbox: new FormControl(false)
  })));

  constructor(private router: Router, private fb: FormBuilder) { }

  
  ngOnInit() {

    let hiddenControl = new FormControl(this.mapItems(this.checkboxGroup.value), Validators.required);

    this.checkboxGroup.valueChanges.subscribe((v) => {
      console.log(v);
      hiddenControl.setValue(this.mapItems(v));
    });

    this.productsForm = new FormGroup({
      items: this.checkboxGroup,
      selectedItems: hiddenControl
    });
    console.log(this.checkboxGroup.controls);
  }

  mapItems(items) {
    let selectedItems = items.filter((item) => item.checkbox).map((item) => item.id);
    return selectedItems.length ? selectedItems : null;
  }

  onSubmit() {
    this.router.navigate(['/result'], { skipLocationChange: true });
  }
}

HTML:

<h3>Select products</h3>
<form [formGroup]="productsForm">
    <div [formArrayName]="items" [class.invalid]="!productsForm.controls.selectedItems.valid">
      <div *ngFor="let control of checkboxGroup.controls; let i = index;" [formGroup]="control">
        <div>
            <input type="checkbox" formControlName="checkbox" id="{{ control.controls.id.value }}">
            <label attr.for="{{ control.controls.id.value }}">{{ control.controls.text.value }}</label>
        </div>
      </div>
    </div>
    <div [class.invalid]="!productsForm.controls.selectedItems.valid" *ngIf="!productsForm.controls.selectedItems.valid">
      Check at least one checkbox!
    </div>
    <hr>
    <pre>{{productsForm.controls.selectedItems.value | json}}</pre>
</form>

In HTML I previously tried to iterate through the FormArray instance using the FormGroup instance like this: productsForm.controls.items.controls. But it used to return an error during compilation which goes like this:

Property 'controls' does not exist on type 'AbstractControl'.

Now I got rid of that error and the project compiles, but now it returns

ERROR Error: Cannot find control with name: '[object Object],[object Object],[object Object]

which isn't helpful at all.

If I console.log checkboxGroup.controls, it returns my checkboxes the way I want it to.

I'm using Angular 10.

Please help!

1

1 Answers

0
votes

Can't call it a good answer, per se, so I'm still open to better answers, but this one forces the code to work.

I came back to iterating with productsForm.controls.items.controls, but I also created a getter that looks like this:

 get checkboxControls() {
    // assign type 'any' to this.productsForm.controls.items which previously was 'AbstractControls'
    let checkbox = this.productsForm.controls.items as any
    // now you can magically call the 'controls' property and iterate, congratulations
    return checkbox.controls
  }

In HTML on line 4 swap checkboxGroup.controls for our new checkboxControls getter:

<div *ngFor="let control of checkboxControls; let i = index;" [formGroup]="control">
        <div>
            <input type="checkbox" formControlName="checkbox" id="{{ control.controls.id.value }}">
            <label attr.for="{{ control.controls.id.value }}">{{ control.controls.text.value }}</label>
        </div>
</div>

Here's a demo link if anyone wants to play with it. Works for both Angular 9 and 10. Good luck!