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 changedpristine
: Whether the control's value has not changed (opposite of dirty)valid
: Whether the control passes all its validation checksinvalid
: Whether the control fails any validation checkpending
: 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!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)