angularformsvalidationrequiredng-class

Changing validations of a select field dynamically in Angular


I am encountering an issue with my Angular application where I'm dynamically changing the required attribute of a select field in a form after it has been submitted.

evaluations.component.html

<form
  novalidate
  #form="ngForm"
  (ngSubmit)="form.form.valid && onSubmit()">
  <div *ngFor="let evaluation of evaluations; index as index">
    <select
      name="evaluator{{index}}"
      id="evaluator{{index}}"
      #evaluatorField="ngModel"
      [(ngModel)]="evaluatorId"
      [required]="isEvaluatorRequired"
      [ngClass]="{'is-invalid': form.submitted && evaluatorField.invalid}">
      <option *ngFor="let evaluator of evaluators" [ngValue]="evaluator._id">
        {{evaluator.name}}
      </option>
    </select>
    ...
  </div>
  <div>
    <input type="radio" name="operation" #operation="ngModel" id="operation-suggested" [value]="'APPROVE'"
      [(ngModel)]="operation" (ngModelChange)="onOperationChange($event)" required />
    <input type="radio" name="operation" #operation="ngModel" id="operation-decline" [value]="'DECLINE'"
      [(ngModel)]="operation" (ngModelChange)="onOperationChange($event)" required />
    ...
  </div>
  ...
  <button type="submit" id="save">
    Save
  </button>
</form>

isEvaluatorRequired changes on the call to onOperationChange(...) done when the operation value changes.

evaluations.component.ts

...
onOperationChange(operation: OperationStatus) {
  this.isEvaluatorRequired = [OperationStatus.APPROVE,/*...*/].includes(operation);
}
...

The problem arises when I change the operation value after submitting the form, which in turn modifies the isEvaluatorRequired value. This triggers the following error:

ERROR Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.
Previous value: 'true'. Current value: 'false'. Expression location: EvaluationsComponent.component. 

Which indicates that the problem is with isEvaluatorRequired, here: [required]="isEvaluatorRequired".

I simplified the example to share only the elements around the issue. I want to know why is it giving the error altough it changes the behavior correctly?


Update Dec 9, 2023

I thought the problem was with the binding in [required], but, actually the problem was here:

[ngClass]="{'is-invalid': form.submitted && formEvaluator.invalid}"

And it had the wrong behavior as well. Why is is not working?


Update Dec 11, 2023

I solved it by replacing formEvaluator.invalid with isEvaluatorRequired && !evaluatorId which is manual validation of the field and it worked. But I still want to know why the formEvaluator.invalid reference cannot be used after the validation changes.


Solution

  • The error message indicates that isEvaluatorRequired is changed after change detection runs. One way to avoid this is to use a Signal or Subject and the AsyncPipe:

    [required]="isEvaluatorRequired$ | async"
    

    Or you could force change detection after updating isEvaluatorRequired:

    // ChangeDetectorRef needs to be injected:
    private cdr = inject(ChangeDetectorRef);
    
    // After changing isEvaluatorRequired:
    this.cdr.detectChanges();