Angular Reactive Forms: FormGroup, FormBuilder, FormArray and Validators
Reactive forms in Angular 12: building the form in the class with FormControl, FormGroup and FormBuilder, custom and async validators, dynamic fields with FormArray, valueChanges and patchValue.
Version note: The code and commands in this post are for Angular 12.
The Form Moves Into the Class
In the template-driven forms we looked at earlier, all the work happened in the template. You wrote ngModel, added validators as attributes, and Angular quietly built the form object for you behind the scenes. For a small contact form, that is great. But as the number of fields grows, as one field's value starts affecting another, or as the user opens new fields with an "add another" button, the template starts to bloat.
Reactive forms flip this around. You build the form's structure in TypeScript, inside the class, and the template simply binds to that structure. Think of an architect: with template-driven forms you build the house on site by eye; with reactive forms you draw the plan at the desk first and the builders follow it. Because the plan is in your hands, you can measure it, change it, and test it at any time.
In this post we will build a small profile form: full name, email, username, and a list of skills that the user can add dynamically.
ReactiveFormsModule and Your First FormControl
The first step is adding ReactiveFormsModule to the module. For template-driven forms we used FormsModule; the reactive side comes with a module of its own.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { ProfileFormComponent } from './profile-form/profile-form.component';
@NgModule({
declarations: [AppComponent, ProfileFormComponent],
imports: [BrowserModule, ReactiveFormsModule],
bootstrap: [AppComponent]
})
export class AppModule { }
Now let's start with the smallest building block. The thing that represents a single field is called FormControl:
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
@Component({
selector: 'app-search-box',
template: `
<input type="text" [formControl]="query">
<p>Searching for: {{ query.value }}</p>
`
})
export class SearchBoxComponent {
query = new FormControl('');
}
new FormControl('') creates a control with an empty initial value. [formControl]="query" in the template binds the input to that object. As the user types, query.value updates; if you call query.setValue('angular') in code, the input's content changes. The value no longer lives in the template but in an object inside the class.
Bringing Fields Together With FormGroup
Real forms are rarely a single field. To gather several FormControl instances under one roof, we use FormGroup:
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
@Component({
selector: 'app-profile-form',
templateUrl: './profile-form.component.html'
})
export class ProfileFormComponent {
profileForm = new FormGroup({
fullName: new FormControl(''),
email: new FormControl('')
});
onSubmit(): void {
console.log(this.profileForm.value);
}
}
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<label>Full name <input formControlName="fullName"></label>
<label>Email <input formControlName="email"></label>
<button type="submit">Save</button>
</form>
[formGroup] binds the form element to the group in the class. Each input inside finds its own key in the group through formControlName. There is no name attribute or ngModel here; the matching happens entirely through key names. Click Save and you will see a plain object like { fullName: '...', email: '...' } in the console.
FormBuilder and Validators
Writing new FormControl(...) for every field gets noisy after a while. The FormBuilder service lets you build the same structure with less code. While we are at it, let's add validators:
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-profile-form',
templateUrl: './profile-form.component.html'
})
export class ProfileFormComponent {
profileForm = this.fb.group({
fullName: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]]
});
constructor(private fb: FormBuilder) { }
onSubmit(): void {
if (this.profileForm.invalid) {
this.profileForm.markAllAsTouched();
return;
}
console.log(this.profileForm.value);
}
}
FormBuilder arrives through the constructor injection we saw earlier. In each array inside fb.group, the first element is the initial value and the second is the list of validators. The Validators class is the function form of the rules you wrote as attributes in template-driven forms: required, minLength, maxLength, email, pattern and more.
markAllAsTouched() marks every field as touched when an invalid form is submitted, so the user also sees errors on fields they never clicked. You can show the error messages in the template like this:
<input formControlName="email">
<p class="error" *ngIf="profileForm.get('email')?.touched && profileForm.get('email')?.hasError('required')">
Email is required.
</p>
<p class="error" *ngIf="profileForm.get('email')?.touched && profileForm.get('email')?.hasError('email')">
Please enter a valid email address.
</p>
get('email') looks up the control in the group. Because it might not find one, its return type is AbstractControl | null, and with strict mode on you need ?.. hasError tells you whether a specific validator is failing. The CSS classes you saw with template-driven forms, such as ng-touched and ng-invalid, are added here in exactly the same way.
Writing Your Own Validator
The built-in validators will not cover every need. Say we don't want spaces in the username. A validator is really just a plain function: it takes the control and returns null if everything is fine, or an error object if it is not.
import { AbstractControl, ValidationErrors } from '@angular/forms';
export function noWhitespace(control: AbstractControl): ValidationErrors | null {
const value = (control.value || '') as string;
return value.includes(' ') ? { whitespace: true } : null;
}
The key of the object you return becomes the name you ask about in the template with hasError('whitespace'). Using it is no different from the built-in ones: username: ['', [Validators.required, noWhitespace]].
Some checks can't answer right away, though. The answer to "is this username taken?" usually lives on the server. For that you write an async validator, and this time the function returns an observable:
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { delay, map } from 'rxjs/operators';
const takenUsernames = ['admin', 'root', 'mert'];
export function usernameAvailable(): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> =>
of(takenUsernames.includes(control.value)).pipe(
delay(500),
map(taken => (taken ? { usernameTaken: true } : null))
);
}
Here we faked the server with of and delay; in a real project an HttpClient call goes in their place. Async validators go in as the third element of the fb.group array: username: ['', [Validators.required, noWhitespace], [usernameAvailable()]]. Angular runs the sync validators first and only starts the async one if they pass. While the answer is pending, the control's status is PENDING, and you can use the pending property in the template to show a "checking..." message.
Dynamic Fields With FormArray
We want users to add their skills one by one, and we don't know in advance how many there will be. FormArray exists for exactly this: an ordered list of controls whose length can change.
profileForm = this.fb.group({
fullName: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
username: ['', [Validators.required, noWhitespace], [usernameAvailable()]],
skills: this.fb.array([])
});
get skills(): FormArray {
return this.profileForm.get('skills') as FormArray;
}
addSkill(): void {
this.skills.push(this.fb.control('', Validators.required));
}
removeSkill(index: number): void {
this.skills.removeAt(index);
}
<div formArrayName="skills">
<div *ngFor="let skill of skills.controls; let i = index">
<input [formControlName]="i" placeholder="Skill">
<button type="button" (click)="removeSkill(i)">Remove</button>
</div>
</div>
<button type="button" (click)="addSkill()">Add skill</button>
The skills getter gives us the array from the group with the right type; don't forget to import FormArray. In the template, formArrayName steps into the array, and each input inside is bound by index rather than by name, as [formControlName]="i". Writing type="button" on the buttons matters; without it, every button inside a form behaves like a submit button by default. The form's value now contains an array like skills: ['TypeScript', 'RxJS'].
Listening to the Form With valueChanges
This is where reactive forms earn their name. Every FormControl, FormGroup and FormArray has a valueChanges observable that emits the new value every time it changes. Let's save the form as a draft whenever it changes:
import { OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
export class ProfileFormComponent implements OnInit, OnDestroy {
// profileForm, skills, constructor ... (as above)
private destroy$ = new Subject<void>();
ngOnInit(): void {
this.profileForm.valueChanges
.pipe(debounceTime(500), takeUntil(this.destroy$))
.subscribe(value => {
localStorage.setItem('profileDraft', JSON.stringify(value));
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
The RxJS operators we saw earlier slot right in. debounceTime(500) waits until the user stops typing for half a second, so we don't save on every keystroke. takeUntil(this.destroy$) closes the subscription when the component is destroyed. With the same approach you can turn another field's validators on and off based on a value, or refresh a city list when a country is selected.
setValue and patchValue
There are two ways to fill a form from code, and the difference is small but important:
// Throws: Must supply a value for form control with name: 'username'.
this.profileForm.setValue({ fullName: 'Jane Doe', email: 'jane@example.com' });
// Works: updates only the given fields
this.profileForm.patchValue({ fullName: 'Jane Doe', email: 'jane@example.com' });
setValue is strict: it wants a value for every field in the group and throws if one is missing. It is useful when you want to be sure you are filling the whole form. patchValue is flexible; it updates the fields you pass and leaves the rest alone. It is the right tool for putting a partial user object from the server into the form, or for restoring the draft we saved above.
One small trap: patchValue writes into a FormArray only as many values as there are existing controls. If the draft has three skills, you first need to open three controls with addSkill().
Why Are Form Values any in Angular 12?
Hover over this.profileForm.value and you will see that its type is any. The value of a control you get with get('email') and the values valueChanges emits are any too. Reactive forms in Angular 12 are untyped. If you accidentally write fullname instead of fullName, the compiler won't warn you; you find the bug at runtime.
In practice you can soften this by casting the form's value to your own interface: const profile = this.profileForm.value as Profile;. That is not a guarantee, just a promise you make to the compiler. We will see how Angular closed this gap in the upcoming version posts.
A Small Experiment
Add a "Contact me by phone" checkbox and a phone field to the profile form. When the checkbox is checked, the phone field should be required; when it is unchecked, the requirement should go away. Subscribe to the checkbox control's valueChanges, use setValidators and clearValidators on the phone control, and don't forget to call updateValueAndValidity() afterwards.
When you are done, temporarily add {{ profileForm.status }} and {{ profileForm.value | json }} to the template and watch the form's state change live as you play with the checkbox. Type "admin" into the username field and try to catch the PENDING status as well.
Have you ever wondered how the screen updates on every keystroke while you fill in a form? How does Angular hear about a changed value, and which component does it re-check, and when? Next up is change detection and Zone.js, the piece that triggers it behind the scenes.