Lesson 23 of 30
FormGroup, FormControl, and FormBuilder model your form as an explicit object tree in the component class — still what most real Angular codebases reach for today.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Reactive Forms model a form as an explicit tree of objects you build in TypeScript: a FormControl wraps a single field's value and validation state, a FormGroup bundles several controls together, and FormBuilder is a small factory that saves you from writing new FormControl(...) everywhere. The template then just binds to that tree — formGroup, formControlName — instead of owning the state itself.
That's the key difference from template-driven forms (ngModel, two-way binding directly on inputs): with Reactive Forms, the source of truth lives in your component class as a real, inspectable, typed object, not scattered across template bindings. That makes validation logic, conditional fields, and testing all easier to reason about — which is why Reactive Forms are still what most real Angular codebases reach for, even with Signal Forms now a stable alternative.
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-signup-form',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="submit()">
<input formControlName="email" placeholder="Email" />
@if (form.controls.email.invalid && form.controls.email.touched) {
<p class="error">Enter a valid email.</p>
}
<input formControlName="password" type="password" placeholder="Password" />
@if (form.controls.password.hasError('minlength')) {
<p class="error">At least 8 characters.</p>
}
<button type="submit" [disabled]="form.invalid">Sign up</button>
</form>
`,
})
export class SignupFormComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
submit() {
if (this.form.invalid) return;
console.log(this.form.getRawValue());
}
}
this.fb.group({...}) builds a FormGroup where each key becomes a FormControl, seeded with an initial value and a validators array. [formGroup]="form" on the <form> element connects the whole tree to the template; formControlName="email" connects one input to one control inside it. Angular keeps each control's .value, .valid, .touched, and .errors up to date as the user types, and the template reads those directly — form.controls.email.invalid, form.invalid on the submit button.
ReactiveFormsModule in imports. In a standalone component, formControlName and [formGroup] come from that module — without it in the imports array, the template throws instead of silently doing nothing.formControlName with ngModel. They belong to two different paradigms — reactive vs. template-driven — and Angular will warn (or misbehave) if you bind both to the same input. Pick one form style per form..value when disabled controls are involved. .value skips disabled controls entirely; .getRawValue() includes them. If a field mysteriously vanishes from your submitted payload, check whether it was disabled.This is the current default that the signal forms lesson's newer, signal-based system is positioned to eventually complement or replace — same underlying problem (structured, validated form state), different reactive primitive underneath. It also pairs naturally with strongly-typed generics from the TypeScript track: FormGroup's type parameters give you compile-time-checked .value and .controls access instead of loosely-typed form data.