Feature Description
The error-state machinery in Angular Material is hardwired to AbstractControl, with no path for the new Signal Forms:
ErrorStateMatcher.isErrorState(control, form) accepts only AbstractControl | null.
CdkStep.stepControl is typed @Input() stepControl!: AbstractControl;.
MatStep registers itself as the ErrorStateMatcher ({ provide: ErrorStateMatcher, useExisting: MatStep }) and ORs the in-scope matcher with its own control.invalid && this.interacted rule.
Signal Forms expose field state as a Field / FieldTree<T> / FieldState, not an AbstractControl. Angular does build an adapter internally — InteropNgControl (created by the Field
directive, wrapping FieldState to look like NgControl/AbstractControl) — but it is not exported from @angular/forms/signals, so consumers can't reuse it.
The result: there is no supported way to drive Material's error state (and MatStep in particular) from a Signal Forms field. Today the only option is to fake an AbstractControl:
// userland workaround — wrap a signal-forms FieldTree so ErrorStateMatcher accepts it
class FieldAbstractControl<T> extends AbstractControl<T> {
constructor(private readonly field: FieldTree<T>) { super(null, null); }
override get invalid() { return this.field().invalid(); }
override get touched() { return this.field().touched(); }
override get dirty() { return this.field().dirty(); }
// setValue/patchValue/reset → no-ops; state lives in the FieldTree
}
This works only because the built-in matchers and MatStep happen to read just invalid/touched/dirty. It's brittle (a future matcher reading status/errors/value would get
super(null, null) defaults) and forces every Signal Forms user to re-discover the same hack.
Use Case
I'm migrating reactive forms to Signal Forms in an app that uses MatStepper. I want each step's header to show the error indicator using the same rules
Material already applies — i.e. honoring MatStep's invalid && interacted behavior so a step only flags an error after the user has moved past it, rather than reading
field().invalid() directly and re-implementing that logic myself.
Because MatStep decides this through ErrorStateMatcher.isErrorState(control, form) and that only accepts an AbstractControl, I have to wrap my FieldTree in a fake AbstractControl
to feed it in. The same need shows up anywhere error state is shown for a signal-forms field — mat-error, custom validity indicators in accordions/lists, etc. A supported Field
→ AbstractControl bridge (or a matcher overload that accepts a field) would let Signal Forms integrate with Material's error-state components without faking control instances,
and keep error semantics defined in exactly one place.
Environment: Angular v21.2.x
Feature Description
The error-state machinery in Angular Material is hardwired to
AbstractControl, with no path for the new Signal Forms:ErrorStateMatcher.isErrorState(control, form)accepts onlyAbstractControl | null.CdkStep.stepControlis typed@Input() stepControl!: AbstractControl;.MatStepregisters itself as theErrorStateMatcher({ provide: ErrorStateMatcher, useExisting: MatStep }) and ORs the in-scope matcher with its owncontrol.invalid && this.interactedrule.Signal Forms expose field state as a
Field/FieldTree<T>/FieldState, not anAbstractControl. Angular does build an adapter internally —InteropNgControl(created by the Fielddirective, wrapping
FieldStateto look likeNgControl/AbstractControl) — but it is not exported from@angular/forms/signals, so consumers can't reuse it.The result: there is no supported way to drive Material's error state (and
MatStepin particular) from a Signal Forms field. Today the only option is to fake anAbstractControl:This works only because the built-in matchers and
MatStephappen to read just invalid/touched/dirty. It's brittle (a future matcher reading status/errors/value would getsuper(null, null)defaults) and forces every Signal Forms user to re-discover the same hack.Use Case
I'm migrating reactive forms to Signal Forms in an app that uses
MatStepper. I want each step's header to show the error indicator using the same rulesMaterial already applies — i.e. honoring
MatStep's invalid && interacted behavior so a step only flags an error after the user has moved past it, rather than readingfield().invalid()directly and re-implementing that logic myself.Because
MatStepdecides this throughErrorStateMatcher.isErrorState(control, form)and that only accepts anAbstractControl, I have to wrap myFieldTreein a fakeAbstractControlto feed it in. The same need shows up anywhere error state is shown for a signal-forms field —
mat-error, custom validity indicators in accordions/lists, etc. A supportedField→
AbstractControlbridge (or a matcher overload that accepts a field) would let Signal Forms integrate with Material's error-state components without faking control instances,and keep error semantics defined in exactly one place.
Environment: Angular v21.2.x