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 aWritableSignalwith reactive validation. Returns aFieldTree.- 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()— nostatusChangesobservable needed. - Works seamlessly with
linkedSignal,computed, andeffectfor 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