How to troubleshoot ExpressionChangedAfterItHasBeenCheckedError: Previous value for 'ng-valid': 'true'. Current value: 'false'

Viewed 761

Can someone help me troubleshoot this. It happens when the form becomes valid, but then I go back and init this component (push a new value in the FormArray (from zero items)). How do I figure out which property ng-valid is triggering on? Is it the array? individual element in array? or the entire formgroup?

I've tried ngAfterContentChecked() { this.cdr.detectChanges(); } and changeDetection: ChangeDetectionStrategy.OnPush, and calling this.cdr.detectChanges() after a push call. Still doesn't work. It's weird because it only happens the first time the form is valid, and then I add a an element.

@Component({
  selector: 'app-list-other-conditions',
  templateUrl: './list-other-conditions.component.html',
  styleUrls: ['../survey-pmhx.component.scss']
})

export class ListOtherConditionsComponent implements OnInit, OnDestroy {

  @Input()
  formGroup!: FormGroup

  @Input()
  arrayName!: string

  get formArray():FormArray {
    return this.formGroup!.get(this.arrayName)! as FormArray
  }

  addItem() {
    this.formArray.push(this.initItem())
  }
  removeItem(i:number) {
    this.formArray.removeAt(i)
    if (this.formArray.length === 0)
      this.formArray.push(this.initItem())
  }

  public initItem = () : FormGroup =>
    this.fb.group({
      diagnosis: this.fb.control(null, Validators.required),
      year: this.fb.control(null, [Validators.required, CustomValidators.pastYear])
    })

  constructor(private fb: FormBuilder, private readonly cdr: ChangeDetectorRef) { }

  ngOnInit(): void {
    if (this.formArray.length === 0)
      this.addItem();
  }
  }


}

1 Answers

I repeated the logic of your code starting a FormGroup in the child component, although I consider it unnecessary and it is not giving me an error, I also create a child FormGroup and start the view of the child component which does not seem necessary but does not cause an error so Therefore, the error must be when injecting the FormArray as a string although it is also possible to inject it as a string to the value of the FormArray, you are not doing it correctly

code in github use path about

import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Observable } from 'rxjs';
import { TeamManagementService } from '../empleados/team-management.service';

@Component({
  selector: 'app-list-other-condition',
  templateUrl: './list-other-condition.component.html',
  styleUrls: ['./list-other-condition.component.css']
})
export class ListOtherConditionComponent implements OnInit {
  @Input()
  formGroup!:FormGroup
  
  @Input()
  employees!:  FormArray
  allSkills!: Observable<any[]>;
  constructor(private fbchild: FormBuilder,private teamMngService: TeamManagementService, private fb: FormBuilder, private readonly cdr: ChangeDetectorRef){
    this.allSkills = this.teamMngService.getSkills()
  }
  
  ngOnInit(): void {
    if (this.employees.length === 0)
    this.addEmployee();

  }
  get age() { return this.employees.get('age'); }

  get empName() { return this.employees.get('empName'); }
 
    addEmployee() {
        let fg = this.createEmpFormGroup();
        this.employees.push(fg);
    }
  deleteEmployee(idx: number) {
        this.employees.removeAt(idx);
    }
  
  public errorHandling = (control: string, error: string) => {
    return this.formGroup.controls[control].hasError(error);
  }

  createEmpFormGroup() {
        return this.fb.group({
            //empName: ['', [Validators.required]],
      empName:this.fbchild.control(null, Validators.required),
        //  age: ['', [Validators.required,Validators.min(21)]],
    age:this.fbchild.control(null, [Validators.required,Validators.min(21)]),
        //  skill: ['', [Validators.required]],
    skill:this.fbchild.control(null, Validators.required),
        })}

}

<!-- begin snippet: js hide: false console: true babel: false -->
<p>list-other-condition works!</p>
<form [formGroup]="formGroup">
    <div formArrayName="employees">
        <div *ngFor="let emp of employees.controls; let i = index" [formGroupName]="i" class="employee">
            
          <mat-label>Employee : {{i + 1}}</mat-label>
       
            <mat-label>Name :</mat-label> 
            <mat-form-field >
            <input matInput formControlName="empName">
            
          </mat-form-field>
          <mat-error *ngIf="employees.controls[i].get('empName')?.errors?.required">empName is required</mat-error>
            
            
            
          Age :
            <mat-form-field >
            <input matInput formControlName="age">
            
          </mat-form-field>
          <mat-error   *ngIf="employees.controls[i].get('age')?.errors?.required"> Age required.</mat-error>
          <mat-error *ngIf="employees.controls[i].get('age')?.errors?.min">
           
            Minimum age is 21.
        
</mat-error>


            
            
             
             
             
            
           

          <h2>Skill :</h2>
          <mat-form-field>
            <mat-label>Your skill</mat-label>
            <mat-select  formControlName="skill"required>
              <mat-option *ngFor="let skill of allSkills | async" [value]="skill.name" >
                {{ skill.displayName }}
              </mat-option>
            </mat-select>
            
            
            </mat-form-field>
            <mat-error
              *ngIf="employees.controls[i].get('skill')?.errors?.required"
             
              >You must make a selection </mat-error>
         <p>
            <button mat-flat-button color="primary" type="button" (click)="deleteEmployee(i)">Delete</button>
          </p> 
        
      </div>
      <button mat-flat-button color="primary" type="button" (click)="addEmployee()">Add More Employee</button>
    </div>
    </form>

<h3>Create New Team</h3>
<mat-card>
  <mat-card-header>
    <mat-card-title>Crear Team</mat-card-title>
  </mat-card-header>
  <mat-card-content>
    
  <form [formGroup]="teamForm" (ngSubmit)="onFormSubmit()">
    
   <h2>Team Name :</h2> 
      <mat-form-field >
      <input matInput formControlName="teamName">
     
      </mat-form-field>
    <mat-error>  
      <span *ngIf="!teamName?.valid && teamName?.touched">Please enter Team Name !!!</span>  
  </mat-error>  
    

  
    
    <h2>Employees in Team:</h2>
  
     
 
  
  
      <app-list-other-condition [formGroup]="teamForm" [employees]="employees" ></app-list-other-condition>
      <mat-card-actions>
      <button mat-flat-button color="primary" type="submit" [disabled]="!teamForm.valid">SUBMIT</button>
      </mat-card-actions>
  </form>
</mat-card-content>
  
</mat-card>
<p>Form Status: {{ teamForm.status }}</p>

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormArray, Validators, FormBuilder, AbstractControl } from '@angular/forms';
import { Observable } from 'rxjs';

import { TeamManagementService } from './team-management.service';
import { Team } from './team';
//import { Employee } from './employee';

@Component({
    selector: 'app-team',
    templateUrl: './team-management.component.html',
    styleUrls: ['./team-management.component.css']

})

export class TeamManagementComponent implements OnInit {
    teamForm = {} as FormGroup;
    //isValidFormSubmitted: boolean | null = null;
    allSkills: Observable<any[]>;
    constructor(
        private formBuilder: FormBuilder,
        private teamMngService: TeamManagementService) {
        this.allSkills = this.teamMngService.getSkills();
    }
    ngOnInit() {
        this.teamForm = this.formBuilder.group({
            teamName: ['', Validators.required],
            employees: this.formBuilder.array([])
                
        });
    }
    // teamForm.teamName.errors
    get formControls() { return this.teamForm.controls; }
    get teamName() {
        return this.teamForm.get('teamName');
    }
    get employees(): FormArray {
        return this.teamForm.get('employees') as FormArray;
    }
    //[disabled]="!teamForm.valid"
    onFormSubmit() {
        
    
    
        if (this.teamForm.valid) {
            console.log('form submitted');
          }
        let team: Team = this.teamForm.value;
        this.teamMngService.saveTeam(team);
        this.teamForm.reset();
    }
    resetTeamForm() {
        this.teamForm.reset();
    }
}

import { Injectable } from '@angular/core';
import { of } from 'rxjs';
import { Team } from './team';

const ALL_SKILLS = [
    { name: 'Java', displayName: 'Java' },
    { name: 'Angular', displayName: 'Angular' },
    { name: 'Dot Net', displayName: 'Dot Net' }
];

@Injectable({
    providedIn: 'root'
})
export class TeamManagementService {
    getSkills() {
        return of(ALL_SKILLS);
    }
    saveTeam(team: Team) {
        console.log('------------TEAM------------');
        console.log('Team Name: ' + team.teamName);
        console.log('----- Employee Details -----');
        for (let emp of team.employees) {
            console.log('Emp Name: ' + emp.empName);
            console.log('Emp age: ' + emp.age);
            console.log('Emp Skill: ' + emp.skill);
            console.log('-------------------');
        }
    }
}

   

your problem must be a conceptual error in

the communication between the parent component and the child

It may be that The control becomes invalid because it does not provide the initial value and specifies a required validator as the code of parent component does not have impossible to know or also the following reasons

FormArray provides a way to collect the dynamically created forms in one place. You can access each of the forms using the index and the controls inside it

<app-list-other-conditions [formGroup]="myform" [arrayName]="list">
and child

@Input()
arrayName!:  FormArray

So it can be accessed correctly different from how you defined Although it is also possible to inject it as a string to the FormArray value, you should not be doing it correctly but

@Input ()
arrayName !: string

I have a draft of its use in the folder employees files file team-management.component.ts draft FormArray

error NG0100: Expression has changed after it was checked This is a cautionary mechanism put in place to prevent inconsistencies between model data and UI so that erroneous or old data are not shown to a user on the page. This catches errors where the view is left in an inconsistent state. This can occur, for example, if a method or getter returns a different value each time it is called, or if a child component changes values on its parent. If either of these occur, this is a sign that change detection is not stabilized. Angular throws the error to ensure data is always reflected correctly in the view, which prevents erratic UI behavior or a possible infinite loop. To make sure not to produce the error define the FormArray in the parent component and send it to the child component as FormArray error Expression Changed

there can always be code errors as well as shown in this answer in the forum error required

Related