Angular validators define what is invalid — they set error keys on the FormControl (required, minlength, etc.). They do not control when those errors are shown to the user. Without any additional mechanism, errors would appear immediately when the page loads, before the user has touched anything.
Koobiq solves this with ErrorStateMatcher.
ErrorStateMatcher is a pure display policy. It reads the control's existing errors and the form's submission state, then answers a single question: should the error be visible right now?
Validators and the FormControl remain unchanged — ErrorStateMatcher never adds or removes errors. It only decides when kbq-form-field should reveal the <kbq-error> children and apply the kbq-invalid CSS class.
Registration:
Apply a matcher to all controls inside a component via providers:
@Component({
providers: [kbqErrorStateMatcherProvider(ShowOnFormSubmitErrorStateMatcher)]
})
Override for a single control using the [errorStateMatcher] input (supported by kbq-tag-list, kbq-file-upload, and similar):
<kbq-file-upload [errorStateMatcher]="myMatcher" ... />
Tag list
kbq-tag-list supports ErrorStateMatcher and integrates with kbq-form-field the same way as other controls.
File upload components support ErrorStateMatcher but have their own layout outside of kbq-form-field, so error output is handled differently from input-based controls. See all validation examples on the File upload page.
Use ShowRequiredOnSubmitErrorStateMatcher when you need split behavior:
required error until the form is submitted.Some rules compare more than one field: "confirm the password", "the new password must differ from the old
one", "the end date can't precede the start date". A single FormControl can't see its sibling's value. So
the natural home for a rule like this is a validator on the FormGroup, not on either field.
<kbq-error> and the red border come from one thing: the control's errorState.
errorState is just the return value of ErrorStateMatcher.isErrorState(control, form). The control here
is always the field's own FormControl — the matcher never sees the group.
So a validator can make the group invalid, and the check still says no. No field turns red. No <kbq-error>
appears.
This is not a bug. It's a division of labor: validators decide what is invalid, the matcher decides when and where to show it. The fix belongs on the matcher's side.
Keep the validator on the group — its logic and its error shape stay entirely yours.
ShowOnCrossFieldErrorStateMatcher adds the missing half: it shows a group-level error on the controls that
error concerns.
It needs one thing from you: a function that takes each error and returns the names of the controls it's
about, or null for an error it doesn't recognize. A name can point into a nested group
('passwords.confirm') or a FormArray row, and the rule itself can live on any ancestor group, not just
the immediate parent.
The error only shows once every control it concerns has been touched, or the form is submitted — showing it earlier would turn a pair red while the user is still typing the second value.
That "any ancestor group" reach has a catch: each control only re-checks its own errorState from its own
ngDoCheck. If the named controls sit in separate OnPush components (each with its own formGroupName),
typing in one marks only that component's view dirty — a sibling component showing another named control
won't refresh on its own. Subscribe to the group's statusChanges and call markForCheck() on the affected
views if your form is split up that way.
One matcher instance is enough for any number of rules on a group — the example above puts two rules on one group, and both are shown by the same matcher.
The default === comparator silently fails on dates and other object values — use DateAdapter's
compareDate() / compareDateTime() / sameDate(), or your own comparator, instead.
One case doesn't belong in this pattern: "at least one of these fields must be filled". Don't mark any field invalid for that rule — show a general alert above the form instead. See the validation guide and its "One required field from several" example.
A rule only the server can decide goes on the group as an AsyncValidatorFn. The answer arrives outside any
event on the field's own view — no click, no keystroke, nothing an OnPush view would normally react to — so
it won't re-check itself when the result lands, whether the error ends up on the group or on a child control.
It needs a signal from the group's statusChanges.