Signal Forms

Angular 21 Experimental — Form state built entirely on Signals

About This Feature

Signal Forms (experimental in Angular 21) is a new approach to forms where the model is a WritableSignal and validation is a reactive layer on top. Unlike Reactive Forms, there is no separate "form model" — the signal IS the model.

  • form(modelSignal, schemaFn) — wraps a WritableSignal with reactive validation. Returns a FieldTree.
  • Schema function receives a typed path tree — call validators like required(p.fieldName), email(p.fieldName), minLength(p.fieldName, 3).
  • All validation state is Signal-based: .valid(), .invalid(), .errors() — no statusChanges observable needed.
  • Works seamlessly with linkedSignal, computed, and effect for derived form logic.

Code Example

import { form, required, email, minLength } from '@angular/forms/signals';

interface UserModel { name: string; email: string; password: string; }

// The model is a WritableSignal — Signal Forms wraps it, not the other way
model = signal<UserModel>({ name: '', email: '', password: '' });

// form(WritableSignal, schemaFn) — schema fn receives SchemaPathTree
myForm = form(this.model, (p) => {
  required(p.name);
  minLength(p.name, 3);
  required(p.email);
  email(p.email);          // validates email format
  required(p.password);
  minLength(p.password, 8);
});

// Access reactive field state (all are Signals):
myForm.name.valid()         // Signal<boolean>
myForm.name.invalid()       // Signal<boolean>
myForm.name.errors()        // Signal<ValidationError[]>
myForm.valid()              // Signal<boolean> — entire form

// Each ValidationError has a .kind property:
// 'required', 'email', 'minLength', 'maxLength', 'pattern'

// Update the model to trigger validation:
this.model.update(m => ({ ...m, name: 'Alice' }));

// Or bind via events in the template:
// (input)="model.update(m => ({ ...m, name: $event.target.value }))"

Live Demo — Registration Form

Validation is driven by signals — myForm.fieldName.errors() updates reactively as you type.

Form valid (signal): false
Live signal state (from FieldTree):
myForm.username().valid() = false
myForm.email().valid() = false
myForm.password().valid() = false
myForm().valid() = false