Reactive-forms in Angular is very powerful to handle changes in value, check the statuses, and perform validations on a form-field in particular form because of supporting FormGroupFormControl, and FormArray.

 

But sometimes it is very difficult to organize the hierarchy and inside the FormGroup and FormArray. It might be the case that we want to add FormControl dynamically to FormArray after completion of some operations like the completion of an API call and also want to remove it.

 

This article explains how we can add and remove checkboxes dynamically to and from FormArray and also making a custom validator that checks if at least one checkbox is checked or not. Let’s start.

 

It is not the case that you can only add FormControl to FormArray. You can also add entirely new FormGroup to the FormArray.

For this article we will use component named DynamicFormComponent which renders our Form. I am assuming that you already have an working Angular project and a component to render your Form.

Create FormGroup:

dynamic-form.component.ts

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

@Component({
  selector: 'app-dynamic-form',
  templateUrl: './dynamic-form.component.html',
  styleUrls: ['./dynamic-form.component.css']
})
export class DynamicFormComponent implements OnInit {

   demoForm: FormGroup;
   
   arrayItems: {
     id: number;
     title: string;
   }[];

   constructor(private _formBuilder: FormBuilder) {
      this.demoForm = this._formBuilder.group({
         demoArray: this._formBuilder.array([])
      });
   }
   
   ngOnInit() {
     this.arrayItems = [];
   }
}
  1. Declaration of two properties demoForm and arrayItems. demoForm is our FormGroup which will have our form-fields. arrayItems is an array of object which contains an id and title of the object. We will use the title as a label for our checkbox.
  2. Injection of FormBuilder service to the constructor.
  3. Initialization of demoForm with a FormGroup which has only one control of type FormArray named demoArraydemoArray is initialized with empty array and there are no validators added to it ( We will add our custom validator latter.)
  4. Initialization of property arrayItems as an empty array in ngOnInit().

Bind FormGroup to template:

dynamic-form.component.html

<form [formGroup]="demoForm">
   <div formArrayName="demoArray" 
      *ngFor="let arrayItem of arrayItems; let i=index">
         
      <input [id]="arrayItem.id" type="checkbox"
         [formControl]="demoArray[i]">

      <label [for]="arrayItem.id" class="array-item-title">
         {{arrayItem.title}}</label>
   
   </div>
</form>
  1. Binding of demoForm with formGroup directive which applies to <form> tag.
  2. Binding of demoArray with formArrayName directive. It also iterates over arrayItems so that we can have those many checkboxes to select. It also gets ‘ i ’ as an index.
  3. Binding of checkbox control to demoArray[i] with formControl directive. It uses index ‘ i ’ to get FormControl of that particular index.
  4. Label for selecting a checkbox. We are using the title as a label.
  5. We also used id and for properties on <input> and <label> tag respectively and bind those properties to an id of arrayItem so that if someone clicks on a title, checkbox gets checked or unchecked.

Add and Remove FormControl:

dynamic-form.component.ts:

...
...

export class DynamicFormComponent implements OnInit {
...
...
   
   get demoArray() {
      return this.demoForm.get('demoArray') as FormArray;
   }
   
   addItem(item) {
      this.arrayItems.push(item);
      this.demoArray.push(this._formBuilder.control(false));
   }

   removeItem() {
      this.arrayItems.pop();
      this.demoArray.removeAt(this.demoArray.length - 1);
   }
   
   ...
   ...
}
  1. We use the getter method to get us demoArray as FormArray directly from demoForm. It creates the new property named as demoArray so that we can directly access the array of controls demoArray without accessing FormGroup demoForm.
  2. Creation of the method which adds an item to the end of arrayItems and also adds new FormControl to the end of demoArray and initializes it with false so that checkbox remains unchecked first time.
  3. Creation of the method which removes an item from the end of arrayItems and also removes FormControl from the end of demoArray.

Custom Validator:

dynamic-form.component.ts:

import { FormBuilder, FormGroup, FormArray, ValidatorFn } from '@angular/forms';

...
...
export class DynamicFormComponent implements OnInit {

   constructor(private _formBuilder: FormBuilder) {
      this.demoForm = this._formBuilder.group({
      demoArray: this._formBuilder.array(
                    [],this.minSelectedCheckboxes()
                 )
      });
   }
   ...
   ...

   minSelectedCheckboxes(): ValidatorFn {
 
      const validator: ValidatorFn = (formArray: FormArray) => {
      
         const selectedCount = formArray.controls
            .map(control => control.value)
            .reduce((prev, next) => next ? prev + next : prev, 0);
         
         return selectedCount >= 1 ? null : { notSelected: true };
      };
   
      return validator;
   }
}
  1. Imports ValidatorFn.
  2. Adds a custom validator to the FormArray demoArray.
  3. Declaration of custom validator function minSelectedCheckboxes(). As we applied this validator to FormArray it will automatically get that FormArray ( demoArray in our case ) and also all its controls. Then we go through the controls inside the FormArray and get the count for checked (truecheckboxes. Then if one or more checkboxes are checked we return null which indicates that FormArray is valid. Otherwise, it returns an object having property notSelected set to true, which indicates there is not a single checkbox checked.

That’s it. As we added and removed a FormControl, same way you can also add and remove entire FormGroup to FormArray and can have a custom validator to validate that FormGroup.

 

Thank you.