Signal Forms Just Got Automatic State Classes (And More)
Source: Dev.to
Let’s Explore How This Signal Form Is Built
Styles
The component’s SCSS still targets the classic ng- classes:
input {
/* touched vs untouched */
&.ng-untouched {
background-color: rgba(white, 0.05);
}
&.ng-touched {
background-color: rgba(#007bff, 0.15);
}
/* dirty vs pristine */
&.ng-dirty {
box-shadow: 0 0 0 2px rgba(#007bff, 0.12);
}
/* valid vs invalid */
&.ng-touched.ng-invalid {
border-color: #e53935;
}
&.ng-touched.ng-valid {
border-color: #43a047;
}
/* pending */
&.ng-pending {
border-color: orange;
}
}
Template Walkthrough: The New [field] Directive
The [field] directive binds the input directly to a Field object from the Signal Form, exposing signals for value, touched, dirty, valid, and pending.
A debug panel shows the real‑time state:
@let username = form.username();
Field State
touched: {{ username.touched() }}
dirty: {{ username.dirty() }}
valid: {{ username.valid() }}
pending: {{ username.pending() }}
Manually adding the classic classes would be possible but cumbersome.
TypeScript Deep Dive: Model and form()
import { signal, form } from '@angular/forms/signals';
import { required, minLength } from '@angular/forms';
interface SignUpForm {
username: string;
}
// Writable signal that holds the form’s data
protected model = signal({
username: '',
});
// Connect the model to the Signal Form API
protected form = form(this.model, s => {
s.username(required(), minLength(3));
});
modelis a writable signal representing the source of truth for the form data.form(this.model, …)creates a Signal Form instance, linking the model to validation logic. Thes.usernamebuilder lets us attach validators such asrequired()andminLength().
Restoring Automatic ng-* Classes (and Customizing Them)
Angular 21 introduced the provideStateClasses configuration for Signal Forms. By adding it to the providers array of a component (or globally), the framework automatically adds the classic ng- classes based on each field’s signals.
import { Component } from '@angular/core';
import { provideStateClasses } from '@angular/forms';
@Component({
selector: 'app-signup',
templateUrl: './form.component.html',
styleUrls: ['./form.component.scss'],
providers: [provideStateClasses()] // <-- enables automatic ng-* classes
})
export class FormComponent {
// model and form definitions as shown above
}
Custom Class Prefixes
If you prefer a different naming scheme, provideStateClasses accepts an options object:
providers: [
provideStateClasses({
prefix: 'sf-', // e.g., sf-touched, sf-dirty, …
map: {
touched: 'my-touched',
dirty: 'my-dirty',
valid: 'my-valid',
invalid: 'my-invalid',
pending: 'my-pending'
}
})
]
prefixadds a custom prefix to all generated classes.maplets you rename individual state classes.
With this configuration, the same SCSS can be updated to target the new class names, or you can keep the original selectors if you use the default ng- prefix.
Summary
- Reactive Forms automatically applied
ng-*state classes; Signal Forms initially omitted them. - Angular 21.0.1 re‑introduces automatic class generation via
provideStateClasses. - The provider works out‑of‑the‑box with the classic
ng-prefix or can be customized with a prefix and explicit mappings. - No need to manually bind classes on each control—Angular handles it for you, letting existing styles continue to work after migrating to Signal Forms.