angulartypescriptangular-reactive-formsformarrayformgroups

Angular Reactive Form - How to use FormArray with FormGroup(s)


I am trying to mimic my model in my reactive form. I've set up a few FormGroups to "nest" things that make sense according to my model. I'm having trouble setting up my component to read the values--which suggests that I likely don't have my template set up correctly either.

Depending if I am editing an existing location or creating a new one,

@Input() location: ILocation;

could be undefined. Currently, I am only focused on working with an existing location, so I know location has value.

// location.model.ts

name: string;
...
messaging: [
    {
        email: {
            fromName: string;
            fromAddress: string;
        };
    }
];
...
createdAt?: string;
updatedAt?: string;
deletedAt?: string;

In my template I am using ngClass to give the user validation feedback:

// location.commponent.html

<div formGroupName="messaging">
    <div formGroupName="email">
        ...
        <div [ngClass]="form.get(['messaging', 'email', 'fromName'])!.errors && (form.get(['messaging', 'email', 'fromName'])!.dirty || form.get(['messaging', 'email', 'fromName'])!.touched) ? 'red' : 'green'">
            <input name="fromName"/>
        </div>
        <!-- fromAddress -->
    </div>
</div>

In my component, I am passing the model in with input binding, then setting up my form group(s) and form fields like this:

// location.component.ts

@Input() location: ILocation; 

form: FormGroup;

...

ngOnInit(): void {
    this.form = new FormGroup({name: new FormControl(this.location.name, [Validators.required]),
    messaging: new FormGroup({
    email: new FormGroup({
        fromName: new FormControl(this.location.messaging[0].email.fromName, [Validators.required]),
        fromAddress: new FormControl(this.location.messaging[0].email.fromAddress, [Validators.required]),
        }),
    }),
}

The error I am seeing is:

Cannot read properties of undefined (reading 'email')

If I log out what is in the component:

console.log('messaging: ', this.location.messaging);

// email: {fromName: 'No Reply <noreply@example.com>', fromAddress: 'noreply@example.com'}

I've tried various methods of messaging['email'] or messaging.email messaging[0] but I can't find the correct path.

I am also not sure if I am using the get() method correctly in my template.

How can I set up my form to correctly read/present the data?

Update:

Not surprisingly, a big problem I was having was sending the wrong shape of data back.

In the end, this is the JSON I am trying to create:

[{"email":{"fromName":"No Reply <noreply@example.com>","fromAddress":"noreply@example.com"}}]

It looks like I need to use FormArray to send the proper shape:

messaging: new FormArray([
    new FormGroup({
        email: new FormGroup({
            fromName: new FormControl(this.location.messaging[0].email.fromName, [Validators.required]),
            fromAddress: new FormControl(this.location.messaging[0].email.fromAddress, [Validators.required]),
        }),
    }),
]),

This is causing some trouble in my template as I'm currently doing this: form.get('messaging[0].email.fromAddress')

Resulting in:

Error: Cannot find control with path: 'messaging -> email'

I think I need to somehow loop through the FormArray. This really isn't a dynamic array though. I'll always have email and only fromName and fromAddress.


Solution

  • Yes, you need the FormArray as the messaging is an array.

    You need to iterate every element in FormArray via *ngFor and provide the index (i):

    form.get(['messaging', i, 'email', 'fromName'])
    

    The complete flow of your template form from parent FormGroup to fromName FormControl would be:

    form (FormGroup) --> messaging (FormArray) --> i (FormGroup) --> email (FormGroup) --> fromName (FormControl)

    The HTML template should be:

    <div [formGroup]="form">
      <div
        formArrayName="messaging"
        *ngFor="let control of messaging.controls; let i = index"
      >
        <ng-container [formGroupName]="i">
          <div formGroupName="email">
            ...
            <div
              [ngClass]="
                form.get(['messaging', i, 'email', 'fromName'])!.errors &&
                (form.get(['messaging', i, 'email', 'fromName'])!.dirty ||
                  form.get(['messaging', i, 'email', 'fromName'])!.touched)
                  ? 'red'
                  : 'green'
              "
            >
              <input formControlName="fromName" />
            </div>
            <div>
              <!-- fromAddress -->
              <input formControlName="fromAddress" />
            </div>
          </div>
        </ng-container>
      </div>
    </div>
    
    get messaging(): FormArray {
      return this.form.get('messaging') as FormArray;
    }
    

    Demo @ StackBlitz