angulartypescriptangular-reactive-formscontrolvalueaccessor

Angular 11 - Validating a child form generated in a for loop


I am using Angular Reactive forms. My parent component has a form, an Add Form. Submit and Reset buttons. When I click on Add form it adds a child form called profile-form to the DOM. The profile-form has two fields First Name and Email. The idea is to reuse the profile-form component for any number of times, the add button is clicked. Each form needs to be validated and the parent form should know the validation status of each child form.

My parent form has this HTML to generate the child forms in a for loop.

<div *ngFor="let fg of formList.controls; let infoIndex = index">
    <app-profile-form formControlName="fg" [formList]="formList" 
                      [formIndex]="infoIndex"></app-profile-form>
  </div>

When I look at the console, I seem to be getting an error that formControlName 'fg' is not found in formList.controls. How do I fix this mapping of formControls between my parent and child form, such that the validation works?

Stackblitz here


Solution

  • Lets try to analyze the form.

    You expect below as the end value of the form

    {
      "formList": [  
        {
          "firstName": "",
          "email": ""
        },
        {
          "firstName": "",
          "email": ""
        },
        {
          "firstName": "",
          "email": ""
        }
      ]
    }
    

    In the above we have

    form  => FormGroup : form 
      formList => FormArray : formList 
        1 => FormControl with value {email: '', firstName} : 1
        2 => FormControl with value {email: '', firstName} : 2
        3 => FormControl with value {email: '', firstName} : 3
    

    So in the form we have to have this structure for the form to work

    <form [formGroup]="signupForm" (ngSubmit)="submit()">
      <ng-container formArrayName='formList'>
        <div *ngFor="let fg of formList.controls; let infoIndex = index">
          <app-profile-form [formControlName]="infoIndex" [formIndex]="infoIndex"></app-profile-form>
        </div>
      </ng-container>
    
      <button>Sign Up</button>
      <button type="button" (click)="resetForm()">Reset</button>
    </form>
    
    

    Some other amendments will include

    Changing formListGroupDef

        return this.formBuilder.control(
          {
            firstName: "",
            email: ""
          },
          Validators.required
        );
    

    SignUpForm

        this.signupForm = this.formBuilder.group({
          formList: this.formBuilder.array([
            this.formListGroupDef()
          ])
        });
    

    I have also made a few changes in your ProfileFormComponent

    export class ProfileFormComponent implements ControlValueAccessor, OnDestroy {
      @Input() formIndex: any;
      destroyed$ = new Subject<any>();
      form: FormGroup;
      get firstNameControl() {
        return this.form.controls.firstName;
      }
    
      get emailControl() {
        return this.form.controls.email;
      }
    
      constructor(private formBuilder: FormBuilder) {
        this.form = this.formBuilder.group({
          firstName: ["", Validators.required],
          email: ["", Validators.required]
        });
    
        this.form.valueChanges.pipe(
          filter(({firstName, email}) => firstName.length > 0 || email.length > 0 ),
          takeUntil(this.destroyed$)
        ).subscribe(value => {
          this.onChange(value);
          this.onTouched();
        });
      }
    
      ngOnDestroy() {
        this.destroyed$.next();
      }
    
      onChange: any = () => {};
      onTouched: any = () => {};
    
      registerOnChange(fn) {
        this.onChange = fn;
      }
    
      writeValue(value) {
        if (value) {
          this.form.patchValue(value);
        }
      }
    
      registerOnTouched(fn) {
        this.onTouched = fn;
      }
    
      validate(_: FormControl) {
        return this.form.valid ? null : { profile: { valid: false } };
      }
    

    See Demo