Search code examples
angularangular-reactive-formsformarrayformgroups

Errors when dynamically binding Form Array consisting of multiple Form Groups to a template in reactive forms of Angular 12


I have data consisting of array of person objects being returned from API for which I have to dynamically generate controls and display in Angular 12 app. The data returned is shown below.

{
"type": "Person",
"fields": [
    {
        "name": "fullName",
        "label": "Full name",
        "validation": {
            "rules": {
                "required": true,
                "maxLength": 100
            },
            "errorMessages": {
                "required": "Enter your full name",
                "maxLength": "The full name must not exceed 100 characters"
            }
        }
    },
    {
        "name": "phoneNumber",
        "label": "Phone number",
        "validation": {
            "rules": {
                "required": true,
                "maxLength": 16
            },
            "errorMessages": {
                "required": "Enter a valid phone number",
                "maxLength": "The phone number must not exceed 16 characters"
            }
        }
    },
    {
        "name": "email",
        "label": "Email",
        "validation": {
            "rules": {
                "required": true,
                "format": "^(([^<>()\\[\\]\\\\.,;:\\s@\"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$"
            },
            "errorMessages": {
                "required": "Enter a valid email address",
                "format": "The email address must be valid"
            }
        }
    }
],
"values": [
    {
        "id": 1,
        "fullName": null,
        "phoneNumber": null,
        "email": null
    },
    {
        "id": 2,
        "fullName": "ytrytrytr",
        "phoneNumber": null,
        "email": null
    },
    {
        "id": 3,
        "fullName": "test",
        "phoneNumber": "2353535",
        "email": "[email protected]"
    }
  ]
}

The code in my component.ts file to fetch the data and create form array and also form groups within that form array is shown below.

form!: FormArray;
personFields!: any;
personValues: any;

ngOnInit() {
     this.personService.getData()
         .subscribe((res: FormObject) => {
        this.personFields = res.fields;
        this.personValues = res.values;
    this.form = new FormArray(this.personValues.map((value: 
    any)=>this.createPersonData(value)));
  });

}

createPersonData(person: any) {
    let personFormGroup = new FormGroup({});

    let validationsArray = [];

    this.personFields.forEach(formField => {
      validationsArray = [];
      if(formField.validation.rules.required) {
        validationsArray.push(Validators.required);
      }
      if(formField.validation.rules.maxLength) {
    
  validationsArray.push(Validators.maxLength(formField.validation.rules.maxLength));
      }
      if(formField.validation.rules.format) {
        validationsArray.push(Validators.pattern(formField.validation.rules.format));
      }

      let formFieldValue = person[formField.name] ? person[formField.name] : null;

      personFormGroup.addControl(formField.name, new FormControl(formFieldValue, 
     validationsArray));
    });

  return personFormGroup;
}

Now, I am binding to the template html file as shown below, lib-input and lib-show-errors are components which I have in an angular library which I am using in this app.

<form *ngIf="form" [formGroup]="form">
    <div *ngFor="let ohFormGroup of form.controls;let i=index">
        <div [formGroup]="ohFormGroup">
            <ng-container *ngFor="let formField of ohFormGroup.controls;">
                <div>
                    <div class="label">{{formField.label}}</div>
                    <lib-input [formControlName]="formField.name">
                    </lib-input>
                </div>
                
                <lib-show-errors *ngIf="isSubmitted && ohFormGroup.controls[formField.name].errors" 
                                [formField]="ohFormGroup.controls[formField.name]" 
                                [errorMessages]="formField.validation.errorMessages">
                </lib-show-errors>
            </ng-container>
        </div>
  </div>
</form>

I need to display controls corresponding to person objects returned in the values array in the JSON. For example if values array has 4 objects, I need to display four sets of Full Name, Phone Number and Email controls which are part of fields array in JSON. If user wants to add a 5th person by clicking a button, I should generate a form group for those 3 controls dynamically and display it and then on submitting the form I need to post all 5 objects to the API POST end point. That is my requirement.

I am facing errors with above template. They are

Type 'FormArray' is missing the following properties from type 'FormGroup': registerControl, addControl, removeControl for the line <form *ngIf="form" [formGroup]="form">

Type 'AbstractControl' is missing the following properties from type 'FormGroup': controls, registerControl, addControl, removeControl, and 3 more for the line <div [formGroup]="ohFormGroup">

Property 'controls' does not exist on type 'AbstractControl' for the line <ng-container *ngFor="let formField of ohFormGroup.controls;">

I don't know what exactly is causing above issues. Please help me out with this.


Solution

  • You has a problem with your .html

    First, create a function that return the FormGroup

    group(index:number)
    {
      return this.form.at(index) as FormGroup
    }
    

    Then you can -I put the code with simple input-

        <div *ngFor="let ohFormGroup of form.controls;let i=index">
                <div [formGroup]="group(i)">
                    <!--see that you iterate over "formFiels" not over "ohFormGroup.controls"-->
                    <ng-container *ngFor="let formField of personFields">
                        <div>
                            <div class="label">{{formField.label}}</div>
                            <input [formControlName]="formField.name">
                        </div>
                    </ng-container>
                </div>
        </div>
    

    Yes is as "ugly trick" make a function to return the formGroup, but else Angular in strict mode don't know about "ohFromGroup" is a FormGroup or a FormControl

    NOTE: about your lib-show-errors, the formField sould be some like [formField]="group(i).get(formField.name)"