Signal Forms made me like Angular forms again
For years Angular forms meant picking your poison: template-driven or reactive. Signal Forms quietly ended that war. One model signal, a schema for validation, and field state as signals. It's so good.
Every Angular dev has had the same conversation. New project, someone asks "template-driven or reactive forms?", and you both sigh because you know neither answer is great. Template-driven is easy until validation gets real. Reactive is powerful but you're hand-building FormGroup trees that duplicate the shape of a model you already defined, and the types never quite line up with your interface.
Signal Forms — experimental in v21 — quietly ended that whole debate. You take a signal holding your data, wrap it in form(), and you're done. The form mirrors your model's shape, the values stay in sync automatically, and validation lives in a schema instead of being smeared across the template. The first time I used it I genuinely went "oh, that's it?" — in the good way.
The model is a signal. The form wraps it.
There's no parallel FormControl universe to keep in sync with your data. Your data is the form:
interface LoginData {
email: string;
password: string;
}
loginModel = signal<LoginData>({ email: '', password: '' });
loginForm = form(this.loginModel);
// field tree mirrors the model shape, dot-accessed:
this.loginForm.email;
this.loginForm.password;form() hands you a field tree shaped exactly like your model. Because it's derived from a typed signal, the field tree is typed too — no casting, no get('email') string lookups that blow up at runtime when you rename a field.
Validation is a schema, not template soup
Validation goes in a schema function — the second argument to form(). It runs once at creation, receives a path object for your fields, and you declare rules against those paths:
loginForm = form(this.loginModel, (path) => {
required(path.email);
email(path.email);
debounce(path.email, 500);
required(path.password);
minLength(path.password, 8);
});Read that again — it's just a list of facts about your form. "email is required, must be an email, debounce its validation by 500ms." No Validators.compose, no array-of-validators boilerplate per control, no wiring it into a FormControl constructor. The rules read like a spec because they basically are one.
Field state is signals all the way down
This is the part that makes the template clean. Every field exposes its state as signals — so your template just reads them, and with zoneless change detection the UI updates exactly when one flips:
<input [field]="loginForm.email" />
@if (loginForm.email().touched() && !loginForm.email().valid()) {
<ul class="errors">
@for (err of loginForm.email().errors(); track err.kind) {
<li>{{ err.message }}</li>
}
</ul>
}
<button [disabled]="!loginForm().valid()">Log in</button>valid(), touched(), errors(), disabled() — all signals, on every field and on the form as a whole. No ngModelChange handlers, no subscribing to statusChanges, no manually tracking dirty state. You bind the field, you read the signals, the framework does the rest.
Conditional logic without the mess
The thing that always rotted reactive forms was conditional validation — "this field is required only if that checkbox is on." It usually meant subscribing to valueChanges and imperatively calling setValidators + updateValueAndValidity, which is exactly the kind of code that breaks silently. Signal Forms has applyWhen:
form(this.model, (path) => {
applyWhen(
path,
(value) => value().wantsNewsletter,
(path) => {
required(path.email);
email(path.email);
},
);
});The condition is reactive. When wantsNewsletter flips, the schema applies or unapplies — no manual re-validation, no leaked subscription. Declarative conditional validation was the dream and it's just... here now.
The honest caveat
It's experimental in v21. The API can shift before it stabilizes, so I wouldn't rip out a battle-tested 60-field enterprise reactive form this week. But for new forms, side projects, and anything greenfield? I'm already reaching for it by default. The shape of the API is clearly right, and "it might rename a function before stable" is a very different risk than "this design is wrong."
The boring conclusion
Signal Forms collapses two competing form systems into one that's simpler than either. Your model is a signal, the form wraps it, validation is a readable schema, and every piece of state you used to chase through subscriptions is now a signal you just read. It's the first time in years I've built an Angular form and not felt like I was fighting the framework to do something obvious. That's the whole pitch, and it's enough.