The reason I mentioned about "non onpush" is because with that mode, Angular runs CD for you. And if your code works as expected in default mode, it means, your "onpush" code just needed to trigger CD.
I didn't mean to tell you that you should switch to default mode.
For why it didn't work in your original question:
The form itself, by default run updateValueAndValidity
after initialized, you don't declare any validators => the form status is valid; then you call setErrors which behind the scene, set the from status to false => this cause the inconsistency in the view, hence the ng100 error.
//Edit:
I debugged the flow again and found out that after each control gets added to the form, it will run the updateValueAndValidity function again. Which means, the setErrors will cause the form to be Invalid first, then updateValueAndValidity will make it all Valid again. The inconsistency is still there, just the other way around.
//End Edit
The dirty hack for this is using setTimeout, which I personally don't like and don't recommend. But to give a properly solution, it will require to know the business logic in the code.
Here it is the code I posted in the comment.
setTimeout(() => {
control.setErrors(Object.keys(errors).length ? errors : null);
});
This helps to schedule the setError (which actually, the valid status) to run in the next CD round.
This code doesn't have any CD yet. So yes, the form is valid all the time => That's the reason I tell you to create a button to see the CD in action.
Now we wouldn't want a new button for users to click to see the form update. We need to run the CD in code. So where to put it?
We know the setError trigger the form status so we will schedule it later, and where does the setError get invoked? customValidator
, we will schedule this function to run later so the final code to modify is here:
//instead of run setTimeout in a loop
//we make sure that all setError is scheduled in one run
setTimeout(() => {
customValidator()(form);
this.cd.markForCheck();
})