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 :
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;
}
}
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));
});