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?
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?
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.
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();