Search code examples
angularangular-reactive-formsangular-forms

Angular 12 - Nest-Form component in list trigger NG0100 Error


Version : Angular 12.2 Stackblitz : https://stackblitz.com/edit/angular-ivy-r8cpbh?file=src/app/app.component.html

Hi, I have a project with 3 components type :

  • Parent with a Children list
  • Child with a SubChildren list
  • SubChild standalone component

At Parent level we can add Child

At Child level we can add SubChild

Everything works fine but when I add a new line, sometime (when the valid field change) i've got this message on formGroup.invalid :

NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value for 'disabled': 'false'. Current value: 'true'

I know it is a common error, but this time I didn't find where the pb come from...

Please help.

Parent
   |____Child
           |______SubChild
           |______SubChild
           |______SubChild
   |____Child
           |______SubChild
   |____Child
           |______SubChild
           |______SubChild

Parent ts

 public data!: ParentData;
  public formGroup!: FormGroup;

  get children() {
    return <FormArray>this.formGroup.controls.children;
  }

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.data = {
      parentField1: 'string',
      parentField2: 'string',
      parentHiddenField1: 'string',
      children: []
    };
    this.formGroup = this.toFormGroup(this.data);

    this.formGroup.valueChanges.subscribe(value => console.log(value));
  }

  add() {
    this.children.push(new FormGroup({}));
    this.data.children.push({
      id: 1,
      childField1: 'string',
      subchildren: []
    });
  }

  private toFormGroup(data: ParentData): FormGroup {
    const formGroup = this.fb.group({
      parentField1: [data.parentField1, Validators.required],
      parentField2: [data.parentField2, Validators.required],
      parentHiddenField1: [data.parentHiddenField1],
      children: new FormArray([])
    });

    return formGroup;
  }

  submit(finalData: ParentData) {
    console.log(finalData);
  }
}

Parent Html

<form [formGroup]="formGroup" (ngSubmit)="submit(formGroup.value)">
  <label for="parentField1">Parent Field 1</label>
  <input formControlName="parentField1" />

  <br />

  <label for="parentField2">Parent Field 2</label>
  <input formControlName="parentField2" />



  <div formArrayName="children">
    <button type="button" (click)="add()">+</button>
    <div *ngFor="let child of data.children; let i=index">
      <div>Parent</div>
      <app-test-list-child [formGroup]="children.controls[i] | formGroup" [data]="child">
      </app-test-list-child>

    </div>
  </div>

  <button type="submit" [disabled]="formGroup.invalid">Send</button>
</form>

Child ts

export class TestListChildComponent implements OnInit, AfterViewInit {
  @Input('formGroup')
  public formGroup!: FormGroup;

  @Input('data')
  public data!: ChildData;

  get subchildren() {
    return <FormArray>this.formGroup.get('subchildren');
  }

  constructor(private cd: ChangeDetectorRef) {}

  ngOnInit() {
    this.formGroup.addControl(
      'childField1',
      new FormControl(this.data.childField1 || '', Validators.required)
    );
    this.formGroup.addControl('subchildren', new FormArray([]));
  }
  ngAfterViewInit() {}

  add() {
    this.subchildren.push(new FormGroup({}));
    this.data.subchildren.push({
      childField1: 'test',
      childHiddenField1: '',
      childField2: '',
      id: 1
    });
  }
}

Child html

<div [formGroup]="formGroup">
  <input formControlName="childField1" />
  <button type="button" (click)="add()">++</button>

  <div formArrayName="subchildren">

    <div *ngFor="let child of subchildren.controls; let i = index">
      <div>Child</div>

      <app-test-child [formGroup]="child | formGroup" [data]="data.subchildren[i]">
      </app-test-child>
    </div>
  </div>
</div>

SubChild ts

export class TestChildComponent implements OnInit, AfterViewInit {

  @Input('formGroup')
  public formGroup!: FormGroup;

  @Input('data')
  public data!: SubChildData;

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.toFormGroup(this.data);
  }

  ngAfterViewInit() {

  }

  private toFormGroup(data: SubChildData) {

    this.formGroup.addControl("childField1", new FormControl(data?.childField1 || '', Validators.required));
    this.formGroup.addControl("childField2", new FormControl(data?.childField2 || '', Validators.required));

  }
}

SubChild html

<!-- child-form.component.html -->
SubChild
<div [formGroup]="formGroup">
  <label for="childField1">Child Field 1</label>
  <input formControlName="childField1" />
  <br/>
  <label for="childField1">Child Field 2</label>
  <input formControlName="childField2" />
</div>

Model

export interface ParentData {
  parentField1: string;
  parentField2: string;
  parentHiddenField1: string;
  children: ChildData[];
}

export interface ChildData {
  id: number;
  childField1: string;
  subchildren: SubChildData[];
}

export interface SubChildData {
  id: number;
  childField1: string;
  childField2: string;
  childHiddenField1: string;
}

Pipe


@Pipe({
  name: 'formGroup'
})
export class FormGroupPipe implements PipeTransform {
  transform(value: AbstractControl, ...args: unknown[]): FormGroup {
    return value as FormGroup;
  }
}


Solution

  • I finaly found that the pb was some kind of asynchronous pattern ...

    I add a setTimeout over the Forms creation and now all work nicely.

    Child

    setTimeout(() => {
     this.formGroup.addControl(
          'childField1',
          new FormControl(this.data.childField1 || '', Validators.required)
        );
        this.formGroup.addControl('subchildren', new FormArray([]));
    });
    
    

    SubChild

    setTimeout(() => {
        this.formGroup.addControl("childField1", new FormControl(data?.childField1 || '', Validators.required));
        this.formGroup.addControl("childField2", new FormControl(data?.childField2 || '', Validators.required));
    });