Forms are an essential part of most web applications, whether it’s a simple login form or a complex multi-step survey. Angular’s Reactive Forms Module provides a powerful and flexible way to build dynamic forms that can adapt to user input, validate data, and handle complex scenarios with ease. In this guide, we’ll walk through the process of creating a user registration form in Angular 19 using the Reactive Forms Module. The form will include fields for first name, last name, international phone number, country, state, email ID, password, confirm password, and a checkbox for terms and conditions. We’ll also implement custom form validation and write unit tests to ensure everything works as expected.
Why Use Reactive Forms in Angular?
Reactive Forms in Angular are built around a model-driven approach, where the form structure and behavior are defined in the component class. This approach offers several advantages:
Dynamic Form Control: Easily add or remove form controls at runtime.
Strong Typing: Leverage TypeScript for better type safety and code maintainability.
Custom Validation: Create custom validation logic for complex scenarios.
Reactive Programming: Use observables to react to form changes in real-time.
Prerequisites
Before diving in, make sure you have the following set up:
- Angular CLI installed (npm install -g @angular/cli).
- A basic understanding of Angular and TypeScript.
- An Angular 19 project created (ng new dynamic-forms-app).
Step 1: Set Up Reactive Forms Module
First, you need to import the ReactiveFormsModule into your Angular application. Open your app.module.ts file and add the following import:
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [
// your components
],
imports: [
BrowserModule,
ReactiveFormsModule, // Add this line
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
This makes the Reactive Forms Module available throughout your application.
Step 2: Create a User Registration Form in the Component
Let’s create a user registration form with the following fields:
- First Name
- Last Name
- International Phone Number
- Country
- State
- Email ID
- Password
- Confirm Password
- Terms and Conditions Checkbox
Generate a New Component:
Run the following command to generate a new component:
ng generate component user-registration
Define the Form in the Component:
Open the user-registration.component.ts file and set up the form using FormGroup and FormControl:
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-user-registration',
templateUrl: './user-registration.component.html',
styleUrls: ['./user-registration.component.css']
})
export class UserRegistrationComponent {
registrationForm: FormGroup;
constructor(private fb: FormBuilder) {
this.registrationForm = this.fb.group({
firstName: ['', [Validators.required, Validators.minLength(2)]],
lastName: ['', [Validators.required, Validators.minLength(2)]],
phoneNumber: ['', [Validators.required, Validators.pattern(/^\+[1-9]\d{1,14}$/)]], // E.164 format
country: ['', Validators.required],
state: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required],
acceptTerms: [false, Validators.requiredTrue]
}, { validators: this.passwordMatchValidator });
}
// Custom validator to check if password and confirm password match
passwordMatchValidator(form: FormGroup) {
const password = form.get('password')?.value;
const confirmPassword = form.get('confirmPassword')?.value;
return password === confirmPassword ? null : { mismatch: true };
}
// Handle form submission
onSubmit() {
if (this.registrationForm.valid) {
console.log('Form Submitted:', this.registrationForm.value);
} else {
console.log('Form is invalid');
}
}
}
Step 3: Build the Template
Now, let’s create the HTML template for the form. Open the user-registration.component.html file and add the following code:
<form [formGroup]="registrationForm" (ngSubmit)="onSubmit()">
<!-- First Name -->
<div>
<label>First Name</label>
<input formControlName="firstName" placeholder="Enter first name">
<div *ngIf="registrationForm.get('firstName')?.invalid && registrationForm.get('firstName')?.touched">
<small *ngIf="registrationForm.get('firstName')?.errors?.['required']">First name is required.</small>
<small *ngIf="registrationForm.get('firstName')?.errors?.['minlength']">First name must be at least 2 characters.</small>
</div>
</div>
<!-- Last Name -->
<div>
<label>Last Name</label>
<input formControlName="lastName" placeholder="Enter last name">
<div *ngIf="registrationForm.get('lastName')?.invalid && registrationForm.get('lastName')?.touched">
<small *ngIf="registrationForm.get('lastName')?.errors?.['required']">Last name is required.</small>
<small *ngIf="registrationForm.get('lastName')?.errors?.['minlength']">Last name must be at least 2 characters.</small>
</div>
</div>
<!-- Phone Number -->
<div>
<label>Phone Number</label>
<input formControlName="phoneNumber" placeholder="Enter phone number (e.g., +1234567890)">
<div *ngIf="registrationForm.get('phoneNumber')?.invalid && registrationForm.get('phoneNumber')?.touched">
<small *ngIf="registrationForm.get('phoneNumber')?.errors?.['required']">Phone number is required.</small>
<small *ngIf="registrationForm.get('phoneNumber')?.errors?.['pattern']">Please enter a valid international phone number.</small>
</div>
</div>
<!-- Country -->
<div>
<label>Country</label>
<select formControlName="country">
<option value="">Select Country</option>
<option value="US">United States</option>
<option value="IN">India</option>
<option value="UK">United Kingdom</option>
</select>
<div *ngIf="registrationForm.get('country')?.invalid && registrationForm.get('country')?.touched">
<small *ngIf="registrationForm.get('country')?.errors?.['required']">Country is required.</small>
</div>
</div>
<!-- State -->
<div>
<label>State</label>
<input formControlName="state" placeholder="Enter state">
<div *ngIf="registrationForm.get('state')?.invalid && registrationForm.get('state')?.touched">
<small *ngIf="registrationForm.get('state')?.errors?.['required']">State is required.</small>
</div>
</div>
<!-- Email -->
<div>
<label>Email</label>
<input formControlName="email" placeholder="Enter email">
<div *ngIf="registrationForm.get('email')?.invalid && registrationForm.get('email')?.touched">
<small *ngIf="registrationForm.get('email')?.errors?.['required']">Email is required.</small>
<small *ngIf="registrationForm.get('email')?.errors?.['email']">Please enter a valid email.</small>
</div>
</div>
<!-- Password -->
<div>
<label>Password</label>
<input type="password" formControlName="password" placeholder="Enter password">
<div *ngIf="registrationForm.get('password')?.invalid && registrationForm.get('password')?.touched">
<small *ngIf="registrationForm.get('password')?.errors?.['required']">Password is required.</small>
<small *ngIf="registrationForm.get('password')?.errors?.['minlength']">Password must be at least 8 characters.</small>
</div>
</div>
<!-- Confirm Password -->
<div>
<label>Confirm Password</label>
<input type="password" formControlName="confirmPassword" placeholder="Confirm password">
<div *ngIf="registrationForm.get('confirmPassword')?.invalid && registrationForm.get('confirmPassword')?.touched">
<small *ngIf="registrationForm.get('confirmPassword')?.errors?.['required']">Confirm password is required.</small>
</div>
<div *ngIf="registrationForm.errors?.['mismatch'] && registrationForm.get('confirmPassword')?.touched">
<small>Passwords do not match.</small>
</div>
</div>
<!-- Terms and Conditions -->
<div>
<label>
<input type="checkbox" formControlName="acceptTerms"> I accept the terms and conditions
</label>
<div *ngIf="registrationForm.get('acceptTerms')?.invalid && registrationForm.get('acceptTerms')?.touched">
<small>You must accept the terms and conditions.</small>
</div>
</div>
<!-- Submit Button -->
<button type="submit" [disabled]="registrationForm.invalid">Register</button>
</form>
Step 4: Write Unit Tests
Unit testing is crucial to ensure your form works as expected. Let’s write tests for the UserRegistrationComponent.
Set Up the Test Suite:
Open the user-registration.component.spec.ts file and update it as follows:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule, FormBuilder } from '@angular/forms';
import { UserRegistrationComponent } from './user-registration.component';
describe('UserRegistrationComponent', () => {
let component: UserRegistrationComponent;
let fixture: ComponentFixture<UserRegistrationComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [UserRegistrationComponent],
imports: [ReactiveFormsModule],
providers: [FormBuilder]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(UserRegistrationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('should initialize the form with default values', () => {
expect(component.registrationForm).toBeDefined();
expect(component.registrationForm.get('firstName')?.value).toBe('');
expect(component.registrationForm.get('acceptTerms')?.value).toBeFalse();
});
it('should validate first name as required', () => {
const firstNameControl = component.registrationForm.get('firstName');
firstNameControl?.setValue('');
expect(firstNameControl?.invalid).toBeTrue();
expect(firstNameControl?.errors?.['required']).toBeTruthy();
});
it('should validate password and confirm password match', () => {
const passwordControl = component.registrationForm.get('password');
const confirmPasswordControl = component.registrationForm.get('confirmPassword');
passwordControl?.setValue('password123');
confirmPasswordControl?.setValue('password456');
expect(component.registrationForm.errors?.['mismatch']).toBeTruthy();
});
it('should enable submit button when form is valid', () => {
component.registrationForm.setValue({
firstName: 'John',
lastName: 'Doe',
phoneNumber: '+1234567890',
country: 'US',
state: 'California',
email: 'john.doe@example.com',
password: 'password123',
confirmPassword: 'password123',
acceptTerms: true
});
fixture.detectChanges();
const submitButton = fixture.nativeElement.querySelector('button[type="submit"]');
expect(submitButton.disabled).toBeFalse();
});
});
Run the Tests:
Use the following command to run your tests:
ng test
Ensure all tests pass successfully.
Step 5: Extend the Form
Now that you have a basic user registration form, you can extend it to handle more complex scenarios:
- Dynamic Country-State Dropdowns: Fetch countries and states from an API and populate the dropdowns dynamically.
- Password Strength Meter: Add a custom validator to check password strength.
- Multi-Step Form: Break the form into multiple steps for better user experience.
Conclusion
Angular’s Reactive Forms Module provides a robust way to build dynamic, scalable, and maintainable forms. By leveraging FormGroup, FormControl, and custom validators, you can create forms that adapt to user input and handle complex validation with ease. Additionally, writing unit tests ensures your forms are reliable and bug-free.
Start experimenting with dynamic forms in your Angular projects today, and unlock the full potential of form handling in your applications!
What’s Next?
- Explore Angular’s built-in validators and create custom validators.
- Integrate Reactive Forms with backend APIs for seamless data submission.
- Learn about form state management using Angular’s valueChanges and statusChanges observables.
Happy coding!