Skip to main content

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:

typescript
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:

typescript
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:

html
<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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

html
<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:

typescript
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:

typescript
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:

typescript
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:

html
<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:

typescript
// 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);
html
<!-- 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:

typescript
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:

  1. Explicit form control - The form structure is defined in your component code
  2. Powerful validation - Built-in and custom validators for any scenario
  3. Dynamic form manipulation - Easy to add/remove controls as needed
  4. Reactive programming integration - Observable-based APIs
  5. 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

Practice Exercises

  1. Simple Contact Form: Create a reactive form with name, email, and message fields with appropriate validations.

  2. Dynamic Form: Build a questionnaire form that allows users to add and remove questions dynamically using FormArray.

  3. Multi-step Form: Create a wizard-style form with multiple steps (personal details, contact information, preferences) that maintains state between steps.

  4. Form with Asynchronous Validation: Implement a registration form with a username field that checks if a username is already taken using an asynchronous validator.

  5. 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!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)