Signal Forms Just Got Automatic State Classes (And More)

Published: (December 5, 2025 at 03:00 AM EST)
3 min read
Source: Dev.to

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));
});
  • model is 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. The s.username builder lets us attach validators such as required() and minLength().

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'
    }
  })
]
  • prefix adds a custom prefix to all generated classes.
  • map lets 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.
Back to Blog

Related posts

Read more »

Angular pipes: Time to rethink

Forem Communities Forem !Forem Logohttps://media2.dev.to/dynamic/image/width=65,height=,fit=scale-down,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3...