FormBuilder
FormBuilder is an injectable service that produces FormGroup, FormControl, and FormArray instances from plain object and array literals. It does exactly what the new FormControl(...) constructors do, just with far less ceremony — no repeated new keywords and a flatter, more readable shape. For any form with more than a couple of fields, it’s the idiomatic way to declare the model, and its typed nonNullable variant gives you non-nullable controls without per-control options.
Injecting FormBuilder
FormBuilder ships with ReactiveFormsModule, so importing that module makes the service available. In modern Angular you obtain it with inject() (or constructor injection) inside a standalone component.
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-profile',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './profile.component.html',
})
export class ProfileComponent {
private fb = inject(FormBuilder);
profileForm = this.fb.group({
firstName: ['', Validators.required],
lastName: [''],
email: ['', [Validators.required, Validators.email]],
});
}
Each entry is a tuple: the first element is the initial value, the second (optional) is a synchronous validator or array of validators, and a third (optional) is an async validator or array. That compact [value, validators] form replaces a verbose new FormControl('', { validators: [...] }) call for every field.
The control shorthand
fb.group({...}) walks the literal you pass and builds the matching control tree. A bare value becomes a FormControl; a tuple adds validators; and you can nest a fb.group(...) to create sub-groups.
this.fb.group({
username: ['', Validators.required],
address: this.fb.group({
street: [''],
city: ['', Validators.required],
zip: ['', Validators.pattern(/^\d{5}$/)],
}),
});
| Builder method | Produces | Typical use |
|---|---|---|
fb.control(value, validators?) | FormControl | A single standalone control |
fb.group({...}) | FormGroup | A keyed set of named controls |
fb.array([...]) | FormArray | A dynamic, ordered list of controls |
The nonNullable builder
By default a FormControl built with FormBuilder is nullable: calling reset() sets its value back to null, and its type is string | null. The fb.nonNullable sub-builder fixes both problems at once — every control it creates has nonNullable: true, so reset() restores the initial value and the inferred type drops the | null.
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-signup',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './signup.component.html',
})
export class SignupComponent {
private fb = inject(FormBuilder);
signupForm = this.fb.nonNullable.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
rememberMe: [false],
});
submit(): void {
if (this.signupForm.valid) {
console.log(this.signupForm.value);
}
}
}
Here signupForm.value is typed as Partial<{ email: string; password: string; rememberMe: boolean }> — note string, not string | null. After a reset(), the fields return to '' and false rather than null.
Output:
{ email: '[email protected]', password: 'secret123', rememberMe: true }
Prefer
fb.nonNullable.group(...)for almost every form. Mixing the default builder withnonNullablecontrols is exactly the inconsistency the typed builder exists to remove.
Building a FormArray
fb.array() takes an array of controls, groups, or values and yields a FormArray. Because the builder returns a normal FormArray, you can push and remove entries at runtime to render repeating UI such as a list of phone numbers.
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, FormArray, Validators } from '@angular/forms';
@Component({
selector: 'app-contacts',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './contacts.component.html',
})
export class ContactsComponent {
private fb = inject(FormBuilder);
form = this.fb.nonNullable.group({
name: ['', Validators.required],
phones: this.fb.array([this.fb.control('', Validators.required)]),
});
get phones(): FormArray {
return this.form.controls.phones;
}
addPhone(): void {
this.phones.push(this.fb.control('', Validators.required));
}
removePhone(index: number): void {
this.phones.removeAt(index);
}
}
<form [formGroup]="form">
<input formControlName="name" placeholder="Name" />
<div formArrayName="phones">
@for (phone of phones.controls; track $index) {
<div>
<input [formControlName]="$index" placeholder="Phone" />
<button type="button" (click)="removePhone($index)">Remove</button>
</div>
}
</div>
<button type="button" (click)="addPhone()">Add phone</button>
</form>
FormBuilder versus constructors
The builder is pure sugar — it constructs the same objects you’d get from new FormGroup(...). The difference is readability and consistency.
// With constructors
new FormGroup({
email: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
});
// With the nonNullable builder
this.fb.nonNullable.group({
email: ['', Validators.required],
});
Both produce an identical model. The builder wins when a form has many fields, because the per-control noise disappears and the structure of the form is easier to read at a glance.
Best practices
- Inject
FormBuilderwithinject(FormBuilder)and keep aprivate fbfield for tersethis.fb.group(...)calls. - Reach for
fb.nonNullable.group(...)by default soreset()restores initial values and your value types stay non-nullable. - Use the
[value, validators]tuple shorthand instead of fullFormControloption objects for everyday fields. - Nest
fb.group(...)for logically related fields (like an address) so the model mirrors your data shape. - Build dynamic lists with
fb.array(...)and a typed getter, thenpush/removeAtto mutate the array at runtime. - Remember the builder is only a convenience — drop down to raw constructors when you need an option the tuple form can’t express, such as
updateOn: 'blur'.