I attempted to create my own custom component with basic validation using regex that can be passed as input to the component. There are two scenarios to consider: one where the form is initially empty (new item form) and another where data is already present (edit).
The issue arises when I try to validate fields after the data has been set, which often results in the error 'Expression has changed after it was checked.'. I understand why this check is necessary and why the error occurs, but I am struggling to see the purpose of ngOnChanges if I cannot change the component state from it. In this case, the state change is represented by the 'isValid' boolean flag which causes the borders of the component to turn red.
I have experimented with the following:
- Using this.changeDetect.detectChanges(), before and after ngOnChanges.
- Changing 'text' as a getter/setter pair. Moving the validation logic there resulted in the same error. This concept seems akin to ngOnChanges, so the error was expected.
- ngAfterViewInit does not handle the scenario where the code updates the form (for example, querying from the backend -> filling the edit form). However, no error occurs in this case.
- Setting the changeDetection strategy as push in the @Component decorator.
- Attempting timeouts... no changes observed. If a timeout over 1 second is set, nothing happens. If less than 1 second, an error occurs. This behavior is likely related to the development mode check that seems to occur 1 second after the initial change detection.
This situation feels like a common and straightforward use case, which leads me to believe that I may be overlooking something simple here.
Example usage:
<cx-form-text-input validate=".+" label="License Plate*" [(text)]="ticket.registerPlate" format="uppercase"></cx-form-text-input>
Example component:
@Component({
selector: 'cx-form-text-input',
templateUrl: './form-text-input.component.html',
styleUrls: ['./form-text-input.component.scss']
})
export class FormTextInputComponent implements OnChanges {
@Input()
public text: string;
@Output() textChange = new EventEmitter();
@Input() public validate;
@Input() public label: string = 'DEFAULT LABEL';
@Input() public disabled = false;
@Input() format: string;
public entry: FormEntry;
constructor(private form: FormContext, private changeDetect: ChangeDetectorRef) {
this.entry = form.Join();
}
private applyFormat(text: string): string {
switch (this.format) {
case 'uppercase':
return text.toUpperCase();
default:
return text;
}
}
public isDisabled(): boolean {
return this.disabled || this.form.disabled;
}
public keyboardEvent() {
this.entry.isDirty = true;
this.text = this.applyFormat(this.text);
this.validateData();
this.textChange.emit(this.text);
}
private validateData() {
if (!this.validate) {
this.entry.isValid = true;
return;
}
if (!this.text) {
this.entry.isValid = false;
return;
}
this.entry.isValid = !!this.text.match(this.validate);
}
ngOnChanges(changes: SimpleChanges) {
this.validateData(); // The pain point...
}
}
Error:
EXCEPTION: Error in ./EditFormComponent class EditFormComponent - inline template:6:72 caused by: Expression has changed after it was checked. Previous value: 'true'. Current value: 'false'.
ORIGINAL EXCEPTION: Expression has changed after it was checked. Previous value: 'true'. Current value: 'false'.
ORIGINAL STACKTRACE: