Search code examples
angularformsvalidation

Angular: ExpressionChangedAfterChecked in nested form with validators added by child


I am trying to pass a form in Angular to a child components in Angular. Within the child component I am dynamically adding some controls to the form and using custom controls (children of the child component) with a ControlValueAccessor to display the form, which implement validators. However, I am getting an unexpected ExpressionChangedAfterChecked error due to the validity of the form changing.

I have drilled down the error to this minimal reproduction (which does not even use the custom ControlValueAccessors anymore nor adds the controls dynamically, so the error must lie somewhere else...).

App.Component.ts

@Component({
  selector: 'app-root',
  template: `
    <app-form-container [form]="form"></app-form-container>
    <button [disabled]="!valid">Button</button>`,
  styleUrls: []
})
export class AppComponent  {
  form: FormGroup = new FormGroup({child1: new FormControl(''), child2: new FormControl('value')});
  get valid(): boolean {
    return this.form.valid;
  }
}

FormContainer.Component.ts

@Component({
  selector: 'app-form-container',
  template: `
    <div [formGroup]="form">
      <input required formControlName="child1">
      <input required formControlName="child2">
    </div>`,
  styles: []
})
export class FormContainerComponent {
  @Input()
  form!: FormGroup;
}

This is the order in which the AppComponent is checked and when the error occurs:

app.component.ts:19 AppComponent ngDoCheck
app.component.ts:24 AppComponent valid true
app.component.ts:24 AppComponent valid false
main.ts:6  ERROR Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value for 'disabled': 'true'. Current value: 'false'. Find more at https://angular.io/errors/NG0100
    at throwErrorIfNoChangesMode (core.mjs:8515:11)
    at bindingUpdated (core.mjs:12843:17)
    at Module.ɵɵproperty (core.mjs:13518:9)
    at AppComponent_Template (app.component.ts:8:13)
app.component.ts:19 AppComponent ngDoCheck
app.component.ts:24 AppComponent valid false

I have found a solution to the problem which is to simply do a cdref.detectChanges in the OnInit hook of the app component (and anywhere else where the form is replaced) but I feel like this is kinda "hacky" and there should be a nicer way to do this.


Solution

  • EDIT: If you need to set validators in child, you can use setValidators for the form control(s). Remember to also call updateValueAndValidity. Also do it after the view has been intialized, otherwise you will get the same error. So in child do:

    ngAfterViewInit() {
      this.form.get('child1')?.setValidators(Validators.required);
      this.form.get('child1')?.updateValueAndValidity();
    }
    

    UPDATED STACKBLITZ


    ORIGINAL ANSWER:

    If you are using a reactive form, you should set the validators to the formcontrols, never to the input itself. If you are using template driven forms, i.e ngModel, then use required on the input.

    With this change you get rid of the error, as the validators are set immediately when form is created.

    form: FormGroup = new FormGroup({
      child1: new FormControl('', [Validators.required]), 
      child2: new FormControl('value', [Validators.required])
    });
    

    STACKBLITZ