0

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.

9
  • Your JSON object must match the structure of your FormGroup exactly. Right now it does not. Commented Jun 9, 2021 at 9:45
  • 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. Commented Jun 9, 2021 at 9:50
  • I see that you have arrays in your JSON, but you have no FormArrays in your code Commented Jun 9, 2021 at 9:53
  • Inside ngOnInit() I have this.form = new FormArray(this.personValues.map((value: any)=>this.createPersonData(value))); This is where the FormArray is created. Commented Jun 9, 2021 at 9:55
  • 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 } Commented Jun 9, 2021 at 9:56

2 Answers 2

1

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

Sign up to request clarification or add additional context in comments.

Comments

1

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>

5 Comments

Thanks for the response. Your approach will create different controls for fields array and values array. My requirement is that based on fields array I need to create controls and based on values array I need to assign corresponding values. I also have an Add button on which user can click to add a person with Full Name, Phone Number and Email controls and I need to dynamically create a FormGroup for it and add it to parent FormArray.
I would decide on your ideal JSON structure form group first. If what you posted for the JSON is not ideal, I would figure that out first. It looks like you're trying to come up with metadata to create a form dynamically. If that's the case, there is no need to store those meta fields in a FormGroup.
I need to display controls corresponding to person objects returned in the values array in the JSON. 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 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.
This is not clear from your question. I would revise your question.
Thank you. I have added my requirement to the question now :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.