AngularJSReactive Forms

Reactive Forms

Reactive forms (also called model-driven forms) define the form structure entirely in the component class using FormControl, FormGroup, and FormArray. The template simply binds to that model. This keeps logic in TypeScript where it is testable, type-safe, and composable with RxJS.

Angular 14 made reactive forms fully typed — every control, group, and array carries TypeScript generics that match your data model.

Setup: Importing ReactiveFormsModule

TS
// Standalone component
import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';

@Component({
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './signup.component.html',
})
export class SignupComponent {}

// NgModule
@NgModule({
  imports: [ReactiveFormsModule],
})
export class AppModule {}
Creating a FormGroup

TS
import { Component } from '@angular/core';
import {
  FormGroup,
  FormControl,
  Validators,
  ReactiveFormsModule,
} from '@angular/forms';

@Component({
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './signup.component.html',
})
export class SignupComponent {
  form = new FormGroup({
    name:  new FormControl('', [Validators.required, Validators.minLength(2)]),
    email: new FormControl('', [Validators.required, Validators.email]),
    password: new FormControl('', [
      Validators.required,
      Validators.minLength(8),
    ]),
  });

  onSubmit() {
    if (this.form.valid) {
      console.log(this.form.value);
      // TypeScript knows: { name: string | null, email: string | null, password: string | null }
    }
  }
}

HTML
<!-- signup.component.html -->
<form [formGroup]="form" (ngSubmit)="onSubmit()">

  <label>Name
    <input formControlName="name" />
    <span *ngIf="form.get('name')?.invalid && form.get('name')?.touched">
      Name is required (min 2 characters).
    </span>
  </label>

  <label>Email
    <input type="email" formControlName="email" />
  </label>

  <label>Password
    <input type="password" formControlName="password" />
  </label>

  <button type="submit" [disabled]="form.invalid">Sign Up</button>
</form>
FormBuilder: Less Boilerplate

FormBuilder is a service that provides shorthand methods for creating controls and groups. It is functionally identical to using new FormControl directly.

TS
import { Component, inject } from '@angular/core';
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';

@Component({
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './signup.component.html',
})
export class SignupComponent {
  private fb = inject(FormBuilder);

  form = this.fb.group({
    name:     ['', [Validators.required, Validators.minLength(2)]],
    email:    ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, Validators.minLength(8)]],
    address: this.fb.group({          // nested group
      street: [''],
      city:   ['', Validators.required],
      zip:    [''],
    }),
  });
}
Note
FormBuilder.group() accepts the same first argument as new FormControl(): a default value, an array [value, syncValidators, asyncValidators], or an options object.
Typed Forms (Angular 14+)

Since Angular 14 the form classes are generic. TypeScript infers the shape of your form automatically, but you can also be explicit.

TS
import { FormControl, FormGroup, NonNullableFormBuilder } from '@angular/forms';

// Explicit typing
const form = new FormGroup({
  email:    new FormControl<string>('', { nonNullable: true }),
  age:      new FormControl<number>(0, { nonNullable: true }),
  verified: new FormControl<boolean>(false, { nonNullable: true }),
});

// form.value is typed as: { email: string; age: number; verified: boolean }
// form.getRawValue() returns the same (includes disabled controls)

// NonNullableFormBuilder — all controls default to nonNullable: true
import { inject } from '@angular/core';
const fb = inject(NonNullableFormBuilder);

const loginForm = fb.group({
  email:    ['', Validators.required],   // FormControl<string> (never null)
  password: ['', Validators.required],   // FormControl<string>
});
Dynamic Fields with FormArray

FormArray manages an ordered list of controls that can grow or shrink at runtime — perfect for adding/removing phone numbers, tags, or address lines.

TS
import { Component, inject } from '@angular/core';
import { FormBuilder, FormArray, Validators, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

@Component({
  standalone: true,
  imports: [ReactiveFormsModule, CommonModule],
  template: `
    <form [formGroup]="form">
      <div formArrayName="phones">
        <div *ngFor="let phone of phones.controls; let i = index">
          <input [formControlName]="i" placeholder="Phone number" />
          <button type="button" (click)="removePhone(i)">Remove</button>
        </div>
      </div>
      <button type="button" (click)="addPhone()">+ Add Phone</button>
    </form>
  `,
})
export class ContactComponent {
  private fb = inject(FormBuilder);

  form = this.fb.group({
    name:   ['', Validators.required],
    phones: this.fb.array([this.createPhone()]),
  });

  get phones(): FormArray {
    return this.form.get('phones') as FormArray;
  }

  createPhone() {
    return this.fb.control('', Validators.pattern(/^\+?[\d\s-]{7,15}$/));
  }

  addPhone() {
    this.phones.push(this.createPhone());
  }

  removePhone(index: number) {
    this.phones.removeAt(index);
  }
}
Nested FormGroups

TS
form = this.fb.group({
  personal: this.fb.group({
    firstName: ['', Validators.required],
    lastName:  ['', Validators.required],
    dob:       [''],
  }),
  contact: this.fb.group({
    email: ['', [Validators.required, Validators.email]],
    phone: [''],
  }),
});

HTML
<form [formGroup]="form">
  <fieldset formGroupName="personal">
    <input formControlName="firstName" />
    <input formControlName="lastName" />
  </fieldset>

  <fieldset formGroupName="contact">
    <input formControlName="email" />
    <input formControlName="phone" />
  </fieldset>
</form>
Reacting to Value Changes

Every FormControl, FormGroup, and FormArray exposes a valueChanges observable. Subscribe to it for real-time reactions — auto-save drafts, cascading selects, live search.

TS
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { Subscription, debounceTime, distinctUntilChanged } from 'rxjs';

@Component({ standalone: true, imports: [ReactiveFormsModule], template: `...` })
export class SearchComponent implements OnInit, OnDestroy {
  private fb = inject(FormBuilder);
  private sub!: Subscription;

  form = this.fb.group({ query: [''] });

  ngOnInit() {
    this.sub = this.form.get('query')!.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged(),
    ).subscribe(query => {
      console.log('Search for:', query);
      // call search service here
    });
  }

  ngOnDestroy() { this.sub.unsubscribe(); }
}
Tip
Use Angular's takeUntilDestroyed() (from @angular/core/rxjs-interop) instead of managing subscriptions manually — it automatically unsubscribes when the component is destroyed.
Programmatic Control: setValue and patchValue

TS
// setValue — must provide ALL controls in the group
this.form.setValue({
  name:  'Alice',
  email: 'alice@example.com',
  password: '',         // must include even if empty
});

// patchValue — partial update, omit fields you don't want to change
this.form.patchValue({
  name: 'Alice Updated',  // only name changes, other fields unchanged
});

// Reset — clears values and resets pristine/touched state
this.form.reset();
this.form.reset({ name: 'Default Name', email: '', password: '' });
Enabling and Disabling Controls

TS
const ctrl = this.form.get('email')!;

ctrl.disable();           // disables (value excluded from form.value)
ctrl.enable();            // re-enables

// Conditional — disable billing address if "same as shipping" is checked
this.form.get('sameAsShipping')!.valueChanges.subscribe(same => {
  const billing = this.form.get('billing')!;
  same ? billing.disable() : billing.enable();
});
Warning
Disabled controls are excluded from form.value. Use form.getRawValue() to include disabled field values in the returned object.
Unit Testing Reactive Forms

Because the model is pure TypeScript, reactive forms are easy to test without a DOM.

TS
import { TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { SignupComponent } from './signup.component';

describe('SignupComponent', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [SignupComponent],
    });
  });

  it('should be invalid when empty', () => {
    const fixture = TestBed.createComponent(SignupComponent);
    expect(fixture.componentInstance.form.valid).toBeFalsy();
  });

  it('should be valid with correct data', () => {
    const fixture = TestBed.createComponent(SignupComponent);
    const form = fixture.componentInstance.form;
    form.setValue({ name: 'Alice', email: 'alice@example.com', password: 'secret123' });
    expect(form.valid).toBeTruthy();
  });

  it('should reject short passwords', () => {
    const fixture = TestBed.createComponent(SignupComponent);
    const password = fixture.componentInstance.form.get('password')!;
    password.setValue('abc');
    expect(password.hasError('minlength')).toBeTruthy();
  });
});
Quick Reference

Task

API

Create a control

new FormControl(value, validators)

Create a group

new FormGroup({ ... }) or fb.group({ ... })

Create an array

new FormArray([...]) or fb.array([...])

Get a control

form.get("field") or form.controls.field

Set all values

form.setValue({ ... })

Partial update

form.patchValue({ ... })

Reset form

form.reset()

Watch changes

control.valueChanges (Observable)

Watch status

control.statusChanges (Observable)

Include disabled

form.getRawValue()

Summary: Reactive forms define the form model in TypeScript, making them predictable, testable, and composable with RxJS. Use FormGroup for fixed structures, FormArray for dynamic lists, and valueChanges / statusChanges observables to react to user input. With Angular 14+ typed forms you get full compile-time safety on your form values.