Angular Reactive Forms
Angular provides two approaches to handling user input through forms: template-driven forms and reactive forms. In this guide, we'll focus on Reactive Forms, a model-driven approach that gives you explicit control over form data and validation.
Introduction to Reactive Forms
Reactive forms provide a model-driven approach to handling form inputs and validations. Unlike template-driven forms which rely heavily on directives in the template, reactive forms are defined in your component class. This gives you:
- More control over form validation logic
- Better testability
- More predictable form behavior
- Easier handling of dynamic forms
- Better support for complex validation scenarios
Reactive forms use an explicit approach to managing the form state, making it ideal for complex forms.
Setting up Reactive Forms in Angular
Step 1: Import the ReactiveFormsModule
To use reactive forms, you first need to import the ReactiveFormsModule in your application module:
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
  imports: [
    // other imports
    ReactiveFormsModule
  ],
  // declarations, providers, etc.
})
export class AppModule { }
Step 2: Create a Form Model in Your Component
The heart of reactive forms is the form model you create in your component. This model represents the structure and state of your form:
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
@Component({
  selector: 'app-user-profile',
  templateUrl: './user-profile.component.html'
})
export class UserProfileComponent implements OnInit {
  userForm: FormGroup;
  
  ngOnInit() {
    this.userForm = new FormGroup({
      firstName: new FormControl('', [Validators.required, Validators.minLength(2)]),
      lastName: new FormControl('', [Validators.required, Validators.minLength(2)]),
      email: new FormControl('', [Validators.required, Validators.email]),
      address: new FormGroup({
        street: new FormControl(''),
        city: new FormControl(''),
        zipCode: new FormControl('')
      })
    });
  }
}
Step 3: Connect the Form Model to the Template
Now that you've created the form model in your component, you need to bind it to your template:
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
  <div>
    <label for="firstName">First Name</label>
    <input id="firstName" type="text" formControlName="firstName">
    <div *ngIf="userForm.get('firstName').invalid && userForm.get('firstName').touched">
      <div *ngIf="userForm.get('firstName').errors?.required">First name is required.</div>
      <div *ngIf="userForm.get('firstName').errors?.minlength">First name must be at least 2 characters long.</div>
    </div>
  </div>
  
  <div>
    <label for="lastName">Last Name</label>
    <input id="lastName" type="text" formControlName="lastName">
    <div *ngIf="userForm.get('lastName').invalid && userForm.get('lastName').touched">
      <div *ngIf="userForm.get('lastName').errors?.required">Last name is required.</div>
      <div *ngIf="userForm.get('lastName').errors?.minlength">Last name must be at least 2 characters long.</div>
    </div>
  </div>
  
  <div>
    <label for="email">Email</label>
    <input id="email" type="email" formControlName="email">
    <div *ngIf="userForm.get('email').invalid && userForm.get('email').touched">
      <div *ngIf="userForm.get('email').errors?.required">Email is required.</div>
      <div *ngIf="userForm.get('email').errors?.email">Please enter a valid email address.</div>
    </div>
  </div>
  
  <div formGroupName="address">
    <h3>Address</h3>
    
    <div>
      <label for="street">Street</label>
      <input id="street" type="text" formControlName="street">
    </div>
    
    <div>
      <label for="city">City</label>
      <input id="city" type="text" formControlName="city">
    </div>
    
    <div>
      <label for="zipCode">ZIP Code</label>
      <input id="zipCode" type="text" formControlName="zipCode">
    </div>
  </div>
  
  <button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>
Step 4: Handle Form Submission
Add a method to handle the form submission:
onSubmit() {
  console.log('Form values:', this.userForm.value);
  
  if (this.userForm.valid) {
    // Process form data
    // For example, send it to a server
    this.submitToServer(this.userForm.value);
  }
}
submitToServer(formData) {
  // Example API call
  // this.http.post('api/users', formData).subscribe(...);
  console.log('Submitting to server:', formData);
}
Key Building Blocks of Reactive Forms
FormControl
FormControl is the most basic building block of reactive forms. It tracks the value and validation state of an individual form control:
const nameControl = new FormControl('Initial value', Validators.required);
// Get the current value
console.log(nameControl.value); // 'Initial value'
// Set a new value
nameControl.setValue('New value');
// Check if valid
console.log(nameControl.valid); // true
// Get validation errors
console.log(nameControl.errors); // null if valid, otherwise object with errors
FormGroup
FormGroup aggregates multiple form controls into a single entity:
const userForm = new FormGroup({
  name: new FormControl('', Validators.required),
  email: new FormControl('', [Validators.required, Validators.email])
});
// Get a specific control
const emailControl = userForm.get('email');
// Check if the entire form is valid
console.log(userForm.valid);
// Get the entire form value as an object
console.log(userForm.value); // { name: '', email: '' }
FormArray
FormArray manages a dynamic list of form controls:
import { FormArray } from '@angular/forms';
// In your component class
this.userForm = new FormGroup({
  name: new FormControl(''),
  phones: new FormArray([
    new FormControl(''),
  ])
});
// Add a new phone control
get phoneForms() { 
  return this.userForm.get('phones') as FormArray;
}
addPhone() {
  this.phoneForms.push(new FormControl(''));
}
removePhone(index: number) {
  this.phoneForms.removeAt(index);
}
In your template:
<div formArrayName="phones">
  <div *ngFor="let phone of phoneForms.controls; let i = index">
    <label>Phone {{i + 1}}:</label>
    <input [formControlName]="i">
    <button type="button" (click)="removePhone(i)">Remove</button>
  </div>
  <button type="button" (click)="addPhone()">Add Phone</button>
</div>
Form Validation
Reactive forms offer powerful validation capabilities:
Built-in Validators
Angular provides several built-in validators:
import { Validators } from '@angular/forms';
this.userForm = new FormGroup({
  username: new FormControl('', [
    Validators.required,
    Validators.minLength(4),
    Validators.maxLength(20)
  ]),
  email: new FormControl('', [
    Validators.required,
    Validators.email
  ]),
  age: new FormControl('', [
    Validators.required,
    Validators.min(18),
    Validators.max(99)
  ]),
  password: new FormControl('', [
    Validators.required,
    Validators.pattern(/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/) // At least 8 chars, with letters and numbers
  ])
});
Custom Validators
You can create custom validators for specific validations:
function passwordMatchValidator(form: FormGroup) {
  const password = form.get('password');
  const confirmPassword = form.get('confirmPassword');
  
  if (password.value !== confirmPassword.value) {
    confirmPassword.setErrors({ passwordMismatch: true });
    return { passwordMismatch: true };
  }
  
  return null;
}
// Apply the validator to the form group
this.registrationForm = new FormGroup({
  password: new FormControl('', [Validators.required]),
  confirmPassword: new FormControl('', [Validators.required])
}, { validators: passwordMatchValidator });
Practical Example: Registration Form
Let's build a complete registration form to demonstrate reactive forms:
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators, FormBuilder } from '@angular/forms';
@Component({
  selector: 'app-registration',
  templateUrl: './registration.component.html'
})
export class RegistrationComponent implements OnInit {
  registrationForm: FormGroup;
  submitted = false;
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit() {
    // Using FormBuilder for cleaner syntax
    this.registrationForm = this.fb.group({
      personalDetails: this.fb.group({
        firstName: ['', [Validators.required, Validators.minLength(2)]],
        lastName: ['', [Validators.required, Validators.minLength(2)]],
        email: ['', [Validators.required, Validators.email]]
      }),
      accountDetails: this.fb.group({
        username: ['', [Validators.required, Validators.minLength(5)]],
        password: ['', [
          Validators.required,
          Validators.minLength(8),
          Validators.pattern(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$/)
        ]],
        confirmPassword: ['', Validators.required]
      }, { validators: this.passwordMatchValidator }),
      termsAccepted: [false, Validators.requiredTrue]
    });
  }
  
  passwordMatchValidator(form: FormGroup) {
    const password = form.get('password');
    const confirmPassword = form.get('confirmPassword');
    
    if (confirmPassword.errors && !confirmPassword.errors.passwordMismatch) {
      return;
    }
    
    if (password.value !== confirmPassword.value) {
      confirmPassword.setErrors({ passwordMismatch: true });
    } else {
      confirmPassword.setErrors(null);
    }
  }
  
  onSubmit() {
    this.submitted = true;
    
    if (this.registrationForm.valid) {
      console.log('Registration form data:', this.registrationForm.value);
      // Submit to backend
    }
  }
  
  get f() {
    return this.registrationForm.controls;
  }
  
  get personalDetails() {
    return this.registrationForm.get('personalDetails') as FormGroup;
  }
  
  get accountDetails() {
    return this.registrationForm.get('accountDetails') as FormGroup;
  }
}
And the corresponding template:
<form [formGroup]="registrationForm" (ngSubmit)="onSubmit()">
  <div formGroupName="personalDetails">
    <h3>Personal Details</h3>
    
    <div>
      <label for="firstName">First Name</label>
      <input id="firstName" type="text" formControlName="firstName">
      <div *ngIf="personalDetails.get('firstName').invalid && (personalDetails.get('firstName').touched || submitted)">
        <div *ngIf="personalDetails.get('firstName').errors?.required">First name is required.</div>
        <div *ngIf="personalDetails.get('firstName').errors?.minlength">First name must be at least 2 characters.</div>
      </div>
    </div>
    
    <div>
      <label for="lastName">Last Name</label>
      <input id="lastName" type="text" formControlName="lastName">
      <div *ngIf="personalDetails.get('lastName').invalid && (personalDetails.get('lastName').touched || submitted)">
        <div *ngIf="personalDetails.get('lastName').errors?.required">Last name is required.</div>
        <div *ngIf="personalDetails.get('lastName').errors?.minlength">Last name must be at least 2 characters.</div>
      </div>
    </div>
    
    <div>
      <label for="email">Email</label>
      <input id="email" type="email" formControlName="email">
      <div *ngIf="personalDetails.get('email').invalid && (personalDetails.get('email').touched || submitted)">
        <div *ngIf="personalDetails.get('email').errors?.required">Email is required.</div>
        <div *ngIf="personalDetails.get('email').errors?.email">Please enter a valid email address.</div>
      </div>
    </div>
  </div>
  
  <div formGroupName="accountDetails">
    <h3>Account Details</h3>
    
    <div>
      <label for="username">Username</label>
      <input id="username" type="text" formControlName="username">
      <div *ngIf="accountDetails.get('username').invalid && (accountDetails.get('username').touched || submitted)">
        <div *ngIf="accountDetails.get('username').errors?.required">Username is required.</div>
        <div *ngIf="accountDetails.get('username').errors?.minlength">Username must be at least 5 characters.</div>
      </div>
    </div>
    
    <div>
      <label for="password">Password</label>
      <input id="password" type="password" formControlName="password">
      <div *ngIf="accountDetails.get('password').invalid && (accountDetails.get('password').touched || submitted)">
        <div *ngIf="accountDetails.get('password').errors?.required">Password is required.</div>
        <div *ngIf="accountDetails.get('password').errors?.minlength">Password must be at least 8 characters.</div>
        <div *ngIf="accountDetails.get('password').errors?.pattern">Password must contain at least one letter, one number, and one special character.</div>
      </div>
    </div>
    
    <div>
      <label for="confirmPassword">Confirm Password</label>
      <input id="confirmPassword" type="password" formControlName="confirmPassword">
      <div *ngIf="accountDetails.get('confirmPassword').invalid && (accountDetails.get('confirmPassword').touched || submitted)">
        <div *ngIf="accountDetails.get('confirmPassword').errors?.required">Please confirm your password.</div>
        <div *ngIf="accountDetails.get('confirmPassword').errors?.passwordMismatch">Passwords do not match.</div>
      </div>
    </div>
  </div>
  
  <div>
    <label>
      <input type="checkbox" formControlName="termsAccepted"> I accept the terms and conditions
    </label>
    <div *ngIf="f.termsAccepted.invalid && (f.termsAccepted.touched || submitted)">
      <div *ngIf="f.termsAccepted.errors?.required">You must accept the terms and conditions.</div>
    </div>
  </div>
  
  <button type="submit">Register</button>
</form>
<div *ngIf="submitted && registrationForm.valid">
  <h3>Registration Successful!</h3>
  <p>Thank you for registering with us.</p>
</div>
Form State and Status
Reactive forms track the state and validity of each control:
- touched: Whether the control has been touched (on blur event)
- dirty: Whether the control's value has changed
- pristine: Whether the control's value has not changed (opposite of dirty)
- valid: Whether the control passes all its validation checks
- invalid: Whether the control fails any validation check
- pending: Whether validation is in progress (e.g., async validators)
You can access these properties in your component or template:
// In component
const emailControl = this.userForm.get('email');
console.log('Is valid:', emailControl.valid);
console.log('Is touched:', emailControl.touched);
console.log('Is dirty:', emailControl.dirty);
<!-- In template -->
<div *ngIf="userForm.get('email').invalid && userForm.get('email').touched">
  Email is invalid!
</div>
Form Value Changes and Status Changes
You can subscribe to value and status changes:
import { Component, OnInit, OnDestroy } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
@Component({
  // Component details
})
export class UserFormComponent implements OnInit, OnDestroy {
  userForm: FormGroup;
  valueChangesSubscription: Subscription;
  statusChangesSubscription: Subscription;
  
  ngOnInit() {
    this.userForm = new FormGroup({
      name: new FormControl(''),
      email: new FormControl('')
    });
    
    // Subscribe to value changes
    this.valueChangesSubscription = this.userForm.valueChanges.subscribe(value => {
      console.log('Form values changed:', value);
    });
    
    // Subscribe to status changes
    this.statusChangesSubscription = this.userForm.statusChanges.subscribe(status => {
      console.log('Form status changed:', status); // 'VALID', 'INVALID', or 'PENDING'
    });
  }
  
  ngOnDestroy() {
    // Clean up subscriptions to prevent memory leaks
    if (this.valueChangesSubscription) {
      this.valueChangesSubscription.unsubscribe();
    }
    
    if (this.statusChangesSubscription) {
      this.statusChangesSubscription.unsubscribe();
    }
  }
}
Summary
Angular Reactive Forms provide a powerful, model-driven approach to form handling in your applications. They offer:
- Explicit form control - The form structure is defined in your component code
- Powerful validation - Built-in and custom validators for any scenario
- Dynamic form manipulation - Easy to add/remove controls as needed
- Reactive programming integration - Observable-based APIs
- Better testability - Forms can be tested without the DOM
Reactive forms are particularly well-suited for complex scenarios like dynamic forms, forms with complex validation requirements, and situations where you need fine-grained control over form behavior.
Additional Resources
- Official Angular Reactive Forms Documentation
- Angular Form Validation Guide
- Dynamic Forms in Angular
Practice Exercises
- 
Simple Contact Form: Create a reactive form with name, email, and message fields with appropriate validations. 
- 
Dynamic Form: Build a questionnaire form that allows users to add and remove questions dynamically using FormArray. 
- 
Multi-step Form: Create a wizard-style form with multiple steps (personal details, contact information, preferences) that maintains state between steps. 
- 
Form with Asynchronous Validation: Implement a registration form with a username field that checks if a username is already taken using an asynchronous validator. 
- 
Form with Conditional Validation: Create a form where certain fields are required based on the values of other fields (for example, requiring a company name only if "I represent a company" is checked). 
Happy coding with Angular Reactive Forms!
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!