2

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 <[email protected]>', fromAddress: '[email protected]'}

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 <[email protected]>","fromAddress":"[email protected]"}}]

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.

0

2 Answers 2

4

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

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

2 Comments

Oh my goodness, this was so helpful to get me over the hump. Thank you so much for your explanation!
If only want to change the apparence of the input, Angular add the class ng-invalid, ng-touched... (see the docs) automatically. So you can simply use this class in your .css, e.g. input.ng-invalid.ng-touched{border-color:red} and you can not fight with ngClass. NOTE: when the selector :has will was full supported by the navigators we can transform also the parent of the input
0

Angular 18 you can provide like this.

<ng-container formArrayName="authors">
                        @for (author of formGroup.get('authors'); track $index)
                        {
                        <input
                          class="form-control form-control-user"
                          [formControl]="author"
                        />
                        }
                      </ng-container>

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.