Skip to content
Angular ng forms 4 min read

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 methodProducesTypical use
fb.control(value, validators?)FormControlA single standalone control
fb.group({...})FormGroupA keyed set of named controls
fb.array([...])FormArrayA 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 with nonNullable controls 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 FormBuilder with inject(FormBuilder) and keep a private fb field for terse this.fb.group(...) calls.
  • Reach for fb.nonNullable.group(...) by default so reset() restores initial values and your value types stay non-nullable.
  • Use the [value, validators] tuple shorthand instead of full FormControl option 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, then push/removeAt to 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'.
Last updated June 14, 2026
Was this helpful?