#Three ways to build forms
A form is three problems glued together: state (what the user typed), rules (is it valid, is this field required, is that one disabled) and syncing that state with real <input> elements in both directions. Every Angular form approach solves the same three problems. What differs is where the source of truth lives.
From my notes: in Angular 22, Signal Forms are stable, so Angular now officially has three approaches:
| Approach | Source of truth | You import | Binding in the template |
|---|---|---|---|
| Signal Forms (new, stable in 22) | A signal() holding your data model | FormField from @angular/forms/signals | [formField]="signupForm.email" |
| Reactive Forms | A tree of FormControl / FormGroup objects in the class | ReactiveFormsModule from @angular/forms | [formGroup] + formControlName="email" |
| Template-driven Forms | ngModel directives in the template (they create the controls for you) | FormsModule from @angular/forms | [(ngModel)]="model.email" + name="email" |
A mental model that helps: picture the same sign-up form built by three people. The Signal Forms person writes down the data ({ name, email, … }) and attaches rules to it — the form is a reactive view over that data. The Reactive Forms person builds a control panel object in TypeScript — every field is a FormControl with its own value, status and observables — and plugs the template into it. The template-driven person writes the HTML first and sprinkles ngModel on inputs; Angular quietly builds the control panel behind the scenes.
This chapter builds one realistic sign-up form — name, email, password + confirmation, a business-only company field, a disabled plan field and a list of phone numbers — three times, once per approach. Every behaviour quoted below comes from Vitest specs that type into the real inputs (dispatching input and blur events) in an Angular 22.2 project.
// Signal Forms — one standalone directive
import { FormField, form, required, email } from '@angular/forms/signals';
@Component({ imports: [FormField], … })
// Reactive Forms — a module of directives
import { ReactiveFormsModule, NonNullableFormBuilder, Validators } from '@angular/forms';
@Component({ imports: [ReactiveFormsModule], … })
// Template-driven Forms — a different module of directives
import { FormsModule, NgForm } from '@angular/forms';
@Component({ imports: [FormsModule], … })- Each approach is a set of directives that the template compiler needs to know about, so each must be listed in the component's
imports(see The modern import model). Forget it and the build fails withNG8002: Can't bind to 'formField' / 'formGroup' / 'ngModel' since it isn't a known property of 'input'— I verified all three. - Signal Forms ship from a separate entry point,
@angular/forms/signals.form()and the rule functions (required,email, …) are plain functions you call in the class; onlyFormFieldgoes inimports, because it's the only thing the template uses. ReactiveFormsModuleandFormsModuleare both NgModules that bundle many directives (FormGroupDirective,FormControlName, … orNgModel,NgForm, …) plus the shared validator directives and value accessors. They are not interchangeable:[formGroup]lives only inReactiveFormsModule,ngModelonly inFormsModule.- The table's last column is the whole story of each approach in one attribute: Signal Forms bind a field of a data model, Reactive Forms bind a control object by name, Template-driven Forms bind a property with banana-in-a-box two-way binding (see Two-way binding).