Dynamic nested reactive form: ExpressionChangedAfterItHasBeenCheckedError

Viewed 11648

My reactive form is three component levels deep. The parent component creates a new form without any fields and passes it down to child components.

At first the outer form is valid. Later on a child component adds new form elements with validators (that fail) making the outer form invalid.

I am getting an ExpressionChangedAfterItHasBeenCheckedError error in the console. I want to fix that error.

Somehow this only happens when I add the third level of nesting. The same approach seemed to work for two levels of nesting.

Plunker: https://plnkr.co/edit/GymI5CqSACFEvhhz55l1?p=preview

Parent component

@Component({
  selector: 'app-root',
  template: `
    myForm.valid: <b>{{myForm.valid}}</b>
    <form>
      <app-subform [myForm]="myForm"></app-subform>
    </form>
  `
})
export class AppComponent implements OnInit {
  ...

  ngOnInit() {
    this.myForm = this.formBuilder.group({});
  }
}

Sub component

@Component({
  selector: 'app-subform',
  template: `
    <app-address-form *ngFor="let addressData of addressesData;"
      [addressesForm]="addressesForm">
    </app-address-form>
  `
})
export class SubformComponent implements OnInit {
  ...

  addressesData = [...];

  constructor() { }

  ngOnInit() {
    this.addressesForm = new FormArray([]);
    this.myForm.addControl('addresses', this.addressesForm);
  }

Child component

@Component({
  selector: 'app-address-form',
  template: `
    <input [formControl]="addressForm.controls.addressLine1">
    <input [formControl]="addressForm.controls.city">
  `
})
export class AddressFormComponent implements OnInit {
  ...

  ngOnInit() {
    this.addressForm = this.formBuilder.group({
      addressLine1: [
        this.addressData.addressLine1,
        [ Validators.required ]
      ],
      city: [
        this.addressData.city
      ]
    });

    this.addressesForm.push(this.addressForm);
  }
}

enter image description here

4 Answers
import {ChangeDetectorRef} from '@angular/core';
....

export class SomeComponent {

  form: FormGroup;

  constructor(private fb: FormBuilder,
              private ref: ChangeDetectorRef) {
    this.form = this.fb.group({
      myArray: this.fb.array([])
    });
  }

  get myArray(): FormArray {
    return this.form.controls.myArray as FormArray;
  }

  addGroup(): void {
    const newGroup = this.fb.group({
      prop1: [''],
      prop2: ['']
    });

    this.myArray.push(newGroup);
    this.ref.detectChanges();
  }
}

I had the same scenario and same issue in Angular 9 and above solution works fine. I tweaked it a little bit: synchroinously adding the control without validators and adding the required validators asynchronously... Because I needed the controls immediately otherwise I got an error cannot find formControl ....

This is my solution based on the accepted answer above:

const resolvedPromise = Promise.resolve(null);
@Component({
  selector: 'password-input',
  templateUrl: './password-input.component.html',
  styleUrls: ['./password-input.component.css']
})
export class PasswordInputComponent implements OnInit {
  @Input() parentFormGroup : FormGroup;
  @Input() controlName : string = 'password';
  @Input() placeholder : string = 'Password';
  @Input() label : string = null;

  ngOnInit(): void {
    this.parentFormGroup.addControl(this.controlName, new FormControl(null));
   
    resolvedPromise.then( () => {
      this.parentFormGroup.get(this.controlName).setValidators(Validators.required)
      this.parentFormGroup.get(this.controlName).updateValueAndValidity();
    });
  }

Following the solution of Max Koretskyi, we can use async/await pattern on ngOnInit.

Here is an example:

@Component({
  selector: 'my-sub-component',
  templateUrl: 'my-sub-component.component.html',
})
export class MySubComponent implements OnInit {
  @Input()
  form: FormGroup;

  async ngOnInit() {
    // Avoid ExpressionChangedAfterItHasBeenCheckedError
    await Promise.resolve(); 

    this.form.removeControl('config');
    
    this.form.addControl('config', new FormControl("", [Validator.required]));       
  }
}
Related