Skip to main content
Reactive forms provide a model-driven approach to handling form inputs whose values change over time. They use explicit and immutable data structures to manage the state of forms at a given point in time.

Setting Up Reactive Forms

Import ReactiveFormsModule in your component or module:
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormControl, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-profile',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `...`
})
export class ProfileComponent {}

FormControl

FormControl is the basic building block that tracks the value and validation status of an individual form control.

Creating a FormControl

packages/forms/src/model/form_control.ts
import { FormControl } from '@angular/forms';

// Simple control with initial value
const name = new FormControl('Nancy');
console.log(name.value); // 'Nancy'

// Control with validation
const email = new FormControl('', Validators.required);
console.log(email.status); // 'INVALID'

// Control with options
const username = new FormControl('', {
  validators: Validators.required,
  updateOn: 'blur'
});

Using FormControl in Templates

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

@Component({
  selector: 'app-name',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <label for="name">Name:</label>
    <input id="name" type="text" [formControl]="name">
    <p>Value: {{ name.value }}</p>
  `
})
export class NameComponent {
  name = new FormControl('Nancy');
}

FormControl State and Updates

packages/forms/src/model/form_control.ts
const control = new FormControl('initial');

// Set value
control.setValue('new value');

// Reset to default
control.reset();
console.log(control.value); // null

// Reset to specific value
control.reset('Nancy');
console.log(control.value); // 'Nancy'

// Disable control
control.disable();
console.log(control.status); // 'DISABLED'

// Enable control
control.enable();

Non-Nullable FormControl

packages/forms/src/model/form_control.ts
// By default, controls reset to null
const dog = new FormControl('spot');
dog.reset(); // dog.value is null

// With nonNullable option, controls reset to initial value
const cat = new FormControl('tabby', { nonNullable: true });
cat.reset(); // cat.value is "tabby"

FormGroup

FormGroup tracks the value and validity state of a group of FormControl instances.

Creating a FormGroup

packages/forms/src/model/form_group.ts
import { FormGroup, FormControl, Validators } from '@angular/forms';

const profileForm = new FormGroup({
  firstName: new FormControl('', Validators.required),
  lastName: new FormControl('', Validators.required),
  email: new FormControl('', [Validators.required, Validators.email]),
  age: new FormControl(null, [Validators.min(18), Validators.max(100)])
});

console.log(profileForm.value);
// { firstName: '', lastName: '', email: '', age: null }

Using FormGroup in Templates

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

@Component({
  selector: 'app-profile',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
      <label for="firstName">First Name:</label>
      <input id="firstName" type="text" formControlName="firstName">
      
      <label for="lastName">Last Name:</label>
      <input id="lastName" type="text" formControlName="lastName">
      
      <label for="email">Email:</label>
      <input id="email" type="email" formControlName="email">
      
      <button type="submit" [disabled]="profileForm.invalid">Submit</button>
    </form>
    
    <p>Form Status: {{ profileForm.status }}</p>
    <p>Form Value: {{ profileForm.value | json }}</p>
  `
})
export class ProfileComponent {
  profileForm = new FormGroup({
    firstName: new FormControl('', Validators.required),
    lastName: new FormControl('', Validators.required),
    email: new FormControl('', [Validators.required, Validators.email])
  });
  
  onSubmit() {
    if (this.profileForm.valid) {
      console.log('Form submitted:', this.profileForm.value);
    }
  }
}

Nested FormGroups

packages/forms/src/model/form_group.ts
const userForm = new FormGroup({
  name: new FormControl(''),
  address: new FormGroup({
    street: new FormControl(''),
    city: new FormControl(''),
    state: new FormControl(''),
    zip: new FormControl('')
  })
});

console.log(userForm.value);
// { name: '', address: { street: '', city: '', state: '', zip: '' } }
Template with nested FormGroup:
<form [formGroup]="userForm">
  <input formControlName="name" placeholder="Name">
  
  <div formGroupName="address">
    <input formControlName="street" placeholder="Street">
    <input formControlName="city" placeholder="City">
    <input formControlName="state" placeholder="State">
    <input formControlName="zip" placeholder="ZIP">
  </div>
</form>

FormGroup Methods

packages/forms/src/model/form_group.ts
const form = new FormGroup({
  first: new FormControl('Nancy'),
  last: new FormControl('Drew')
});

// Set all values (strict)
form.setValue({ first: 'John', last: 'Doe' });

// Patch partial values
form.patchValue({ first: 'Jane' });
console.log(form.value); // { first: 'Jane', last: 'Doe' }

// Reset form
form.reset();
console.log(form.value); // { first: null, last: null }

// Get raw value (includes disabled controls)
form.getRawValue();

// Add control dynamically
form.addControl('middle', new FormControl(''));

// Remove control
form.removeControl('middle');

// Check if control exists and is enabled
form.contains('first'); // true

FormArray

FormArray tracks the value and validity state of an array of controls. Use it for dynamic forms where the number of controls is not known in advance.

Creating a FormArray

packages/forms/src/model/form_array.ts
import { FormArray, FormControl, Validators } from '@angular/forms';

const hobbies = new FormArray([
  new FormControl('Reading'),
  new FormControl('Gaming')
]);

console.log(hobbies.value); // ['Reading', 'Gaming']
console.log(hobbies.length); // 2

Dynamic Form Example

import { Component } from '@angular/core';
import { ReactiveFormsModule, FormArray, FormControl, FormGroup } from '@angular/forms';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-contact-list',
  standalone: true,
  imports: [ReactiveFormsModule, CommonModule],
  template: `
    <form [formGroup]="contactForm">
      <div formArrayName="contacts">
        <div *ngFor="let contact of contacts.controls; let i = index">
          <label>Contact {{ i + 1 }}:</label>
          <input [formControlName]="i" placeholder="Phone number">
          <button type="button" (click)="removeContact(i)">Remove</button>
        </div>
      </div>
      <button type="button" (click)="addContact()">Add Contact</button>
    </form>
    <p>Contacts: {{ contacts.value | json }}</p>
  `
})
export class ContactListComponent {
  contactForm = new FormGroup({
    contacts: new FormArray([
      new FormControl('')
    ])
  });
  
  get contacts() {
    return this.contactForm.get('contacts') as FormArray;
  }
  
  addContact() {
    this.contacts.push(new FormControl(''));
  }
  
  removeContact(index: number) {
    this.contacts.removeAt(index);
  }
}

FormArray Methods

packages/forms/src/model/form_array.ts
const arr = new FormArray([
  new FormControl('Nancy'),
  new FormControl('Drew')
]);

// Access control at index
const first = arr.at(0);
console.log(first.value); // 'Nancy'

// Add control to end
arr.push(new FormControl('John'));

// Insert control at index
arr.insert(1, new FormControl('Jane'));

// Remove control at index
arr.removeAt(0);

// Replace control at index
arr.setControl(0, new FormControl('Bob'));

// Clear all controls
arr.clear();
console.log(arr.length); // 0

// Set all values
arr.setValue(['Alice', 'Bob', 'Charlie']);

// Patch values (can be partial)
arr.patchValue(['David']);

FormBuilder

The FormBuilder service provides convenience methods to reduce boilerplate when creating forms.

Basic Usage

packages/forms/src/form_builder.ts
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'app-profile',
  template: `...`
})
export class ProfileComponent {
  constructor(private fb: FormBuilder) {}
  
  // Without FormBuilder
  profileForm1 = new FormGroup({
    firstName: new FormControl(''),
    lastName: new FormControl(''),
    email: new FormControl('')
  });
  
  // With FormBuilder - much cleaner!
  profileForm2 = this.fb.group({
    firstName: [''],
    lastName: [''],
    email: ['']
  });
  
  // With validators
  profileForm3 = this.fb.group({
    firstName: ['', Validators.required],
    lastName: ['', Validators.required],
    email: ['', [Validators.required, Validators.email]]
  });
}

FormBuilder with Nested Groups and Arrays

packages/forms/src/form_builder.ts
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'app-order',
  template: `...`
})
export class OrderComponent {
  constructor(private fb: FormBuilder) {}
  
  orderForm = this.fb.group({
    customerName: ['', Validators.required],
    email: ['', [Validators.required, Validators.email]],
    address: this.fb.group({
      street: [''],
      city: [''],
      state: [''],
      zip: ['', Validators.pattern(/^\d{5}$/)]
    }),
    items: this.fb.array([
      this.fb.group({
        product: [''],
        quantity: [1, Validators.min(1)],
        price: [0]
      })
    ])
  });
  
  get items() {
    return this.orderForm.get('items') as FormArray;
  }
  
  addItem() {
    this.items.push(this.fb.group({
      product: [''],
      quantity: [1, Validators.min(1)],
      price: [0]
    }));
  }
}

NonNullableFormBuilder

packages/forms/src/form_builder.ts
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';

@Component({
  selector: 'app-settings',
  template: `...`
})
export class SettingsComponent {
  constructor(private fb: FormBuilder) {}
  
  // Regular FormBuilder - resets to null
  settingsForm1 = this.fb.group({
    theme: ['dark']
  });
  // After reset: { theme: null }
  
  // NonNullable FormBuilder - resets to initial value
  settingsForm2 = this.fb.nonNullable.group({
    theme: ['dark']
  });
  // After reset: { theme: 'dark' }
}

Reactive Forms Best Practices

FormBuilder reduces boilerplate and makes your form creation code more readable.
interface ProfileForm {
  firstName: FormControl<string>;
  lastName: FormControl<string>;
  email: FormControl<string>;
}

const form = new FormGroup<ProfileForm>({
  firstName: new FormControl('', { nonNullable: true }),
  lastName: new FormControl('', { nonNullable: true }),
  email: new FormControl('', { nonNullable: true })
});
Extract complex form sections into their own components with ControlValueAccessor.
this.form.get('email')?.valueChanges.subscribe(email => {
  // React to email changes
});
const form = this.fb.group({
  description: ['', { updateOn: 'blur' }]
});

Observing Form State

import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';

@Component({
  selector: 'app-reactive-form',
  template: `...`
})
export class ReactiveFormComponent implements OnInit {
  form = this.fb.group({
    name: [''],
    email: ['']
  });
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit() {
    // Listen to value changes
    this.form.valueChanges.subscribe(value => {
      console.log('Form value changed:', value);
    });
    
    // Listen to status changes
    this.form.statusChanges.subscribe(status => {
      console.log('Form status changed:', status);
    });
    
    // Listen to specific control
    this.form.get('email')?.valueChanges.subscribe(email => {
      console.log('Email changed:', email);
    });
  }
}

Next Steps

Form Validation

Learn about built-in and custom validators

Template-Driven Forms

Compare with template-driven approach