0
votes

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.

2
Your JSON object must match the structure of your FormGroup exactly. Right now it does not.pixelbits
I am creating form array and form groups dynamically from the json response and then binding to template. When I log the 'form' created in the console, I can see the form array and the form groups inside it but can't bind them to template. That is the problem I am facing.suvenk
I see that you have arrays in your JSON, but you have no FormArrays in your codepixelbits
Inside ngOnInit() I have this.form = new FormArray(this.personValues.map((value: any)=>this.createPersonData(value))); This is where the FormArray is created.suvenk
You're passing a FormArray to your <form> when you should be psssing a FormGroup. Also, when accessing the controls within a FormArray, you need a method in your TS which type-casts your Array, otherwise Angular thinks it's an AbstractControl (i.e. getFormArray() { return myFormArray as FormArray }Will Alexander

2 Answers

1
votes

Based on your desired JSON structure, I would initialize your form group in the following way:

export class MyComponent extends OnInit {
   @Input()
   person: Person

   formGroup: FormGroup
   ngOnInit() {
      this.formGroup = new FormGroup({
          type: new FormControl(this.person.type),
          fields: new FormArray(
              this.person.fields.map(t => {
                  return this.createField(t)
              }
          ),
          values: new FormArray(
              this.person.values.map(t => {
                  return this.createValue(t)
              })
          )
      })
   }
   createField(field: Field) {
       return new FormGroup({
           name: new FormControl(field.name),
           label: new FormControl(field.label),
           validation: new FormGroup(validation)
       })
   },
   createValue(value: Value) {
       return new FormGroup({
           id: new FormControl(value.id),
           fullName: new FormControl(value.fullName),
           phoneNumber: new FormControl(value.phoneNumber),
           email: new FormControl(value.email)
       })
   }
}

Your template might look like this:

<form *ngIf="formGroup" [formGroup]="formGroup">
    <ng-container *ngIf="formGroup.get('fields'); let fields">
    <div [formArray]="fields">
    <div *ngFor="let field of fields.controls;let i = index">
        <div [formGroupName]="i">
             <div>
                 <div class="label">{{field.label}}</div>
                 <lib-input [formControlName]="field.name">
                 </lib-input>
             </div>
             <lib-show-errors [validation]="field.validation">
             </lib-show-errors>
        </div>
    </div>
    </div>
    </ng-container>
</form>
1
votes

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)"