Skip to content

Reactive Forms: FormGroup and FormControl

Angular has two form systems: template-driven forms (using [(ngModel)]) and reactive forms. Template-driven forms are simple but hard to test and validate. Reactive forms define the form structure in the component class, making validation, testing, and programmatic control straightforward.

In reactive forms, the form structure lives in the TypeScript class — not the template. The template binds to that structure, but the source of truth is the class:

// Class holds the form definition
filterForm = this.fb.group({
genre: [''],
year: [''],
sortBy: ['popularity.desc']
});
// Template binds to it
// <form [formGroup]="filterForm">
// <select formControlName="genre">...</select>

FormBuilder is a service that provides shorthand methods for creating FormGroup and FormControl instances:

import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
@Component({
standalone: true,
imports: [ReactiveFormsModule],
// ...
})
export class Browse {
private fb = inject(FormBuilder);
filterForm = this.fb.group({
genre: [''], // FormControl with initial value ''
year: [''],
sortBy: ['popularity.desc']
});
}

fb.group() creates a FormGroup. Each value in the object is either:

  • A single value (shorthand for a FormControl with that initial value): ['']
  • A tuple with value and validators: ['', [Validators.required]]
  • A nested FormGroup: fb.group({ ... })

Use [formGroup] on the form element and formControlName on inputs:

browse.html
<form [formGroup]="filterForm">
<select formControlName="genre">
<option value="">All Genres</option>
@for (genre of genres; track genre.id) {
<option [value]="genre.id">{{ genre.name }}</option>
}
</select>
<select formControlName="year">
<option value="">All Years</option>
@for (year of years; track year) {
<option [value]="year">{{ year }}</option>
}
</select>
<select formControlName="sortBy">
<option value="popularity.desc">Most Popular</option>
<option value="vote_average.desc">Highest Rated</option>
<option value="release_date.desc">Newest</option>
</select>
</form>

ReactiveFormsModule must be in the component’s imports array.

Access the current value of the form or a specific control:

// Get the whole form value
const filters = this.filterForm.value;
// { genre: 'action', year: '2024', sortBy: 'popularity.desc' }
// Get a specific control
const genreControl = this.filterForm.get('genre');
const currentGenre = genreControl?.value;

Or access controls directly via the typed shorthand (TypeScript knows the shape):

const genre = this.filterForm.controls.genre.value;

valueChanges is an Observable that emits every time the form value changes:

ngOnInit(): void {
this.filterForm.valueChanges.subscribe(values => {
this.loadMovies(values);
});
}

In the next lessons you will learn to add debounceTime and distinctUntilChanged to this stream — avoiding excessive API calls as the user changes filters.

  1. Create a Browse component with ReactiveFormsModule in imports.
  2. Inject FormBuilder and create a searchForm = this.fb.group({ query: [''], category: ['all'] }).
  3. Add <form [formGroup]="searchForm"> with a text input (formControlName="query") and a select (formControlName="category") with a few options.
  4. Add a button that calls a method to log this.searchForm.value to the console.
  5. Change the input values and confirm the logged object updates.
  • Reactive forms define form structure in the component class using FormBuilder, FormGroup, and FormControl.
  • fb.group({ controlName: [initialValue] }) creates a FormGroup.
  • Bind the form to the template with [formGroup]="myForm" on the form element.
  • Bind individual controls with formControlName="name" on inputs.
  • Add ReactiveFormsModule to the component’s imports array.
  • filterForm.valueChanges is an Observable that emits whenever any control changes.