Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Angular 5 – Reactive Forms with Dynamic FormArray and Simple Validation

5.00/5 (4 votes)
28 Apr 2018CPOL6 min read 60K   14  
How to dynamically display controls in FormArray using Angular 5 Reactive Forms and enable/disable the validators based on the selection

angular 5 FormArray

Table of Contents

Introduction

Recently, I was working with Angular 5 Reactive forms to create a registration form. Part of the form requires the checkbox elements to be generated dynamically. So, it could be 3,4,5 or more checkboxes on the form. If the checkbox is checked, a new FormControl/HTML element will appear next to it. That could be a textbox, dropdown or radio button list types in contingent to the checked item and they are required fields. In this article, I will share how to accomplish the following task. If you have a different opinion, please share it and I’ll really appreciate it.

  • Display the checkboxes in the FormArray dynamically
  • Hide and show a form control based on checkbox selection
  • Add and remove required field validator based on checkbox selection
  • Add required field indicator to radio button list
  • Display required field indicators
  • Collect the selected checkbox and dynamic control value

Display Required Field Indicators

Instead of displaying the required field message next to each form element, we can program the form to display an error indicator by using Cascading Style Sheets (CSS) selector. For instance, we can use the ::after selector to append an asterisk next to the label in red font.

Listing 1
CSS
.required::after {
    content: " *";
    color: red;
}

Or make the form control border red color if the control has ng-invalid class. Figure 1 shows the output results by using the CSS in Listing 1 and Listing 2.

Listing 2
CSS
.form-control.ng-invalid {
    border-left:5px solid red;
}
Figure 1

Angular 5 FormGroup Required Field

The radio button list is a little tricky, based on the sample application, we can utilize the CSS combinators and pseudo-classes in Listing 3 to append an asterisk next to the label. Basically, the selector will select all the labels under a form control with invalid state and append an asterisk next to it with red font. Figure 2 shows the form control output using the CSS in listing 3.

Listing 3
CSS
.form-control.ng-invalid ~ label.checkbox-inline::after {
    content: " *";
    color: red;
}
Figure 2

Angular 5 FormGroup Radio button Required Field

Display the Checkboxes in the FormArray Dynamically

Listing 4 shows how to utilize the FormBuilder.group factory method to creates a FormGroup with firstName, lastName, email and programmingLanguage FormControl in it. The first three FormControls are required and the email must match the regular expression requirement. The programmingLanguage values type is a FormArray, which will host an array of available programming languages using Checkboxes and other input type.

Listing 4
JavaScript
this.sampleForm = this.formBuilder.group({
            firstName: new FormControl('', Validators.required),
            lastName: new FormControl('', Validators.required),
            email: new FormControl('', [Validators.required,Validators.pattern(this.regEmail)]),
            programmingLanguage: this.formBuilder.array([{}])
        });

Shown in listing 5 is the sample object and data that the application will be using.

Listing 5
JavaScript
class Item {
   constructor(
        private text: string,
        private value: number) { }
}
class FormControlMetadata {
   constructor(
        private checkboxName: string,
        private checkboxLabel: string,
        private associateControlName: string,
        private associateControlLabel: string,
        private associateControlType: string,
        private associateControlData: Array<Item>) { }
}
this.programmingLanguageList = [ 
        new Item('PHP',1),
        new Item('JavaScript',2),
        new Item('C#',3),
        new Item('Other',4)];     
     this.otherProgrammingLanguageList = [ 
      new Item('Python',1),
      new Item('Ruby',2),
      new Item('C++',3),
      new Item('Rust',4)];
    this.phpVersionList = [ 
      new Item('v4',1),
      new Item('v5',2),
      new Item('v6',3),
      new Item('v7',4)];

The next step is to populate the programmingLanguage FormArray. This FormArray will contain an array of FormGroup and each FormGroup will host multiple FormControl instances. Shown in Listing 6 is the logic use to populate the FormArray and the langControlMetadata object that will be utilized by the HTML template later on. The later object is to store the properties of element/control such as Checkbox, Textbox, Radio button, Dropdown list, etc.…

Initially, the code will loop through the properties of programmingLanguageList object and populate the langControlMetadata object. The Checkbox and associated HTML element name will be the combination of a static text and the Item.value/key from the data source. These properties will be mapped to formControlName in the HTML template. The Checkbox label will come from the Item.text. The associateControlLabel property will serve as a placeholder attribute for the input element. By default, the associateControlType will be a textbox, in this example, the application will display a radio button list if PHP option is checked, and a dropdown list if Other option is checked. The purpose of the associateControlData property is to hold the data source for the radio button and dropdown elements.

The next step is to create two FormControls, one for the Checkbox element and one for the associate element. By default, the associate element is disabled. Then, insert the child controls created previously into a FormGroup. The key will be identical with the Checkbox and associate element name. Finally, insert the FormGroup instance into the programmingLanguage object.

Listing 6
JavaScript
enum ControlType {
    textbox =1 ,
    dropdown = 2,
    radioButtonList = 3
}

export class Common {
  public static ControlType = ControlType;
  public static CheckboxPrefix = 'cbLanguage_';
  public static OtherPrefix ='otherValue_';
}

langControlMetada: Array<FormControlMetadata> = [];

  populateProgrammingLanguage() {
    //get the property
    this.programmingFormArray = this.sampleForm.get('programmingLanguage') as FormArray;    
    //clear
    this.programmingFormArray.removeAt(0);

    let p:Item;
    //loop through the list and create the formarray metadata
    for (p of this.programmingLanguageList) {
      
      let control = new FormControlMetadata();
      let group = this.formBuilder.group({});
      
      //create the checkbox and other form element metadata
      control.checkboxName = `${Common.CheckboxPrefix}${p.value}`;
      control.checkboxLabel = p.text;
      control.associateControlName = `${Common.OtherPrefix}${p.value}`;
      control.associateControlLabel = `${p.text} comments`;
      control.associateControlType = Common.ControlType[Common.ControlType.textbox];
      
      //assume 1 is radio button list
       if (p.value == 1) {
          control.associateControlType = Common.ControlType[Common.ControlType.radioButtonList];
          control.associateControlData = this.phpVersionList;
      }
      
      //just assumed id 4 is dropdown
      if (p.value == 4) {
          control.associateControlType = Common.ControlType[Common.ControlType.dropdown];
          control.associateControlData = this.otherProgrammingLanguageList;
      }
      
      //store in array, use by html to loop through
      this.langControlMetada.push(control);
      
      //form control
      let checkBoxControl = this.formBuilder.control('');
      let associateControl = this.formBuilder.control({ value: '', disabled: true });

      //add to form group [key, control]
      group.addControl(`${Common.CheckboxPrefix}${p.value}`, checkBoxControl);
      group.addControl(`${Common.OtherPrefix}${p.value}`, associateControl);

      //add to form array
      this.programmingFormArray.push(group);
    }
  }

Listing 7 shows how the programmingLanguage FormArray is rendered in the HTML template. It uses the NgFor directive to build the "Your favorite programming language" section with the properties from the langControlMetadata object. The template is very straight forward, the first element is a Checkbox and it has a change event associate with it. The second element, will render as a textbox, radio button or dropdown list, depending on the item.associateControlType value.

Listing 7
HTML
<div class="form-group row" formArrayName="programmingLanguage">
        <div class="col-xs-12"
             *ngFor="let item of langControlMetada; let i = index;">
            <div [formGroupName]="i">
                <div class="form-group row">
                    <div class="form-inline" style="margin-left:15px;">
                        <div class="form-check">
                            <label [for]="item.checkboxName" class="form-check-label">
                                <input type="checkbox" class="form-check-input" 
                                                       [id]="item.checkboxName" 
                                        (change)="languageSelectionChange
                                          (i, item.checkboxName, item.associateControlName)" 
                                        [formControlName]="item.checkboxName"> {{ item.checkboxLabel}}
                            </label>
                            <input *ngIf="item.associateControlType == 'textbox'"
                                   class="form-control form-control-sm"
                                   id="item.associateControlName"
                                   [placeholder]="item.associateControlLabel" maxlength="255"
                                   [formControlName]="item.associateControlName" />
                            <span *ngIf="item.associateControlType == 'dropdown'">
                                <select class="form-control form-control-sm"
                                        [formControlName]="item.associateControlName">
                                    <option *ngFor="let item of item.associateControlData"
                                            [value]="item.value">
                                        {{item.text}}
                                    </option>
                                </select>
                            </span>
                            <span *ngIf="item.associateControlType == 'radioButtonList'">
                                <span *ngFor="let option of item.associateControlData">
                                    <input #version type="radio" [formControlName]=
                                                    "item.associateControlName"
                                           class="form-control form-control-sm"
                                           [value]="option.value">
                                    <label class="checkbox-inline" *ngIf="!version.disabled"> 
                                                                   {{option.text}}</label>
                                </span>
                            </span>
                        </div>
                    </div>
                 </div>
             </div>
         </div>
     </div>

Hide and Show a Form Control Based on Checkbox Selection

Shown in listing 8 is the function to enable and disable, add and remove validators of an element through the checkbox checked change event. If the checkbox is checked, the business logic will use the enable() method to enable the associate control/element and set the field to required. The purpose of the updateValueAndValidity() method is to update the value and validation status of the control. On the other hand, unchecking the checkbox will clear the value of the associate element, disable it and remove the validator.

Listing 8
JavaScript
    languageSelectionChange(pos: number, cnkName: string, txtName: string) {
  let programmingLanguage = this.programmingLanguages();

  let control = programmingLanguage.controls[pos] as FormGroup

  if (control.controls[cnkName].value == true) {
      //checkbox checked
      control.controls[txtName].enable();
      control.controls[txtName].setValidators([Validators.required]);
      control.controls[txtName].updateValueAndValidity();
      this.selectedLanguageCount++;
  }
  else {
      //unchecked
      control.controls[txtName].setValue('');
      control.controls[txtName].disable();
      control.controls[txtName].clearValidators();
      control.controls[txtName].updateValueAndValidity();
      this.selectedLanguageCount--;
  }
}

Shown in listing 9 is the CSS to hide the control with disabled attribute.

Listing 9
CSS
.form-control:disabled {
    display: none;
}

Collect the Selected Checkbox and Dynamic Control Value

Once the form is valid, the submit button will become clickable. We will keep it simple by sending the selected checkbox id and the associate control/element value to the API.

Shown in listing 10 is the code for the button click event. The code will iterate the programmingLanguage control which is the FormArray to get the checked checkbox and associate control values in the FormGroup. Previously, the checkbox and associate control name were being generated by using a combination of a static text and the value/key from the data source. In order to get the checkbox id, first, get the first control name from the FormGroup. Then, remove the static text and parse it to number. After that, use the id to generate the checkbox and associate control name and use it to access the control value in the FormGroup by name.

For instance, if the id is 3, then checkbox and associate control name will be “cbLanguage_3” and “otherValue_3” respectively. Then use the name to query the FormGroup to check if the checkbox value is true, if yes, collect the id and the associate control value. Refer to listing 10 for more details.

Listing 10
JavaScript
programmingLanguages(): FormArray {
    return this.sampleForm.get('programmingLanguage') as FormArray;
  };

public submit(e: any): void {
    e.preventDefault();

    //reset
    let selectedLanguageList: Array<Item> = [];
    let programmingLanguage = this.programmingLanguages();
    let i: number;
    //checkbox id
    let languageId: number = 0;

    for(i = 0; i < programmingLanguage.controls.length; i++) {

        let control = programmingLanguage.controls[i] as FormGroup
        let selectedLanguage: Language = {} as any;

        //get the selected checkbox id
        for (var k in control.controls) {
            languageId = Number(k.replace(/[a-zA-Z_]/g, ""));
            break;
        }

        //capture the selected checkbox Id and textbox value
        if (control.controls[`${Common.CheckboxPrefix}${languageId}`].value == true) {
            selectedLanguage.value = languageId;
            selectedLanguage.text = control.controls[`${Common.OtherPrefix}${languageId}`].value
            selectedLanguageList.push(selectedLanguage);
        }
    }

    if (selectedLanguageList.length == 0) {
        this.missingLanguage = true;
    } else {
        //submit to API
        let formObjectToApi = new FormControlMetadata();

        formObjectToApi.lastName = this.sampleForm.controls['lastName'].value;
        formObjectToApi.firstName = this.sampleForm.controls['firstName'].value;
        formObjectToApi.email = this.sampleForm.controls['email'].value;
        formObjectToApi.selectedLanguages = selectedLanguageList;

        this.missingLanguage = false;
        this.test = formObjectToApi;
    }
}

Conclusion

I hope someone will find this information useful and that it will make your programming job easier. If you find any bugs or disagree with the contents or want to help improve this article, please drop me a line and I'll work with you to correct it. I would suggest you visit the demo site and explore it in order to grasp the full concept because I might have missed some important information in this article. Please contact me if you want to help improve this article.

History

  • 04/15/2018 - Initial version
  • 04/28/2018 - Updated links to the demo site

Watch this Script in Action

Resources

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)