Reactive Forms in Angular offer a model-driven approach to handling form input and validation. Unlike Template-driven forms, Reactive Forms provide more control, better testing capabilities and these are more suitable for complex form scenarios.
Setting Up the Form
//signup.component.ts
import { ReactiveFormsModule } from '@angular/forms';
@Component({
// ...
imports: [ReactiveFormsModule]
})
Form Structure and Implementation
import { Component } from '@angular/core';
import { AbstractControl, FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
function equalValues(controlName1: string, controlName2: string) {
return (control: AbstractControl) => {
const val1 = control.get(controlName1)?.value
const val2 = control.get(controlName2)?.value
if(val1 === val2){
return null
}
return {valueNotEqual: true}
}
}
@Component({
selector: 'app-signup',
standalone: true,
templateUrl: './signup.component.html',
styleUrl: './signup.component.css',
imports: [ReactiveFormsModule]
})
export class SignupComponent {
//Create new form instance
form = new FormGroup({
email: new FormControl("",{
validators: [Validators.email, Validators.required]
}),
passwords: new FormGroup({
password: new FormControl("", {
validators: [Validators.required, Validators.minLength(6)]
}),
confirmPassword: new FormControl("", {
validators: [Validators.required, Validators.minLength(6)]
}),
}, {
validators: [
equalValues('password', 'confirmPassword')
]
}),
firstName: new FormControl("", {validators: [Validators.required]}),
lastName: new FormControl("", {validators: [Validators.required]}),
address: new FormGroup({
street: new FormControl("", {validators: [Validators.required]}),
number: new FormControl("", {validators: [Validators.required]}),
postalCode: new FormControl("", {validators: [Validators.required]}),
city: new FormControl("", {validators: [Validators.required]}),
}),
role: new FormControl<
'student' | 'teacher' | 'employee' | 'founder' | 'other'
>('student', {validators: [Validators.required]}),
source: new FormArray([
new FormControl(false),
new FormControl(false),
new FormControl(false),
]),
agree: new FormControl(false, {validators: [Validators.required]})
})
onSubmit() {
if(this.form.invalid){
console.log('INVALID FORM')
return;
}
console.log(this.form)
}
onReset() {
this.form.reset()
}
}
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<h2>Welcome on board!</h2>
<p>We just need a little bit of data from you to get you started 🚀</p>
<div class="control">
<label for="email">Email</label>
<input id="email" type="email" name="email" formControlName = "email" />
</div>
<div class="control-row" formGroupName="passwords">
<div class="control">
<label for="password">Password</label>
<input
id="password"
type="password"
name="password"
formControlName = "password"
/>
</div>
<div class="control">
<label for="confirm-password">Confirm Password</label>
<input
id="confirm-password"
type="password"
name="confirm-password"
formControlName="confirmPassword"
/>
</div>
</div>
<hr />
<div>
<div class="control-row">
<div class="control">
<label for="first-name">First Name</label>
<input
type="text"
id="first-name"
name="first-name"
formControlName="firstName"
/>
</div>
<div class="control">
<label for="last-name">Last Name</label>
<input
type="text"
id="last-name"
name="last-name"
formControlName="lastName"
/>
</div>
</div>
<fieldset formGroupName="address">
<legend>Your Address</legend>
<div class="control-row">
<div class="control">
<label for="street">Street</label>
<input type="text" id="street" name="street"
formControlName="street"
/>
</div>
<div class="control">
<label for="number">Number</label>
<input type="text" id="number" name="number"
formControlName="number"
/>
</div>
</div>
<div class="control-row">
<div class="control">
<label for="postal-code">Postal Code</label>
<input type="text" id="postal-code" name="postal-code"
formControlName="postalCode"
/>
</div>
<div class="control">
<label for="city">City</label>
<input type="text" id="city" name="city"
formControlName="city"
/>
</div>
</div>
</fieldset>
</div>
<hr />
<div class="control-row">
<div class="control">
<label for="role">What best describes your role?</label>
<select id="role" name="role" formControlName="role">
<option value="student">Student</option>
<option value="teacher">Teacher</option>
<option value="employee">Employee</option>
<option value="founder">Founder</option>
<option value="other">Other</option>
</select>
</div>
</div>
<fieldset formArrayName="source">
<legend>How did you find us?</legend>
<div class="control">
<input
type="checkbox"
id="google"
name="acquisition"
value="google"
formControlName="0"
/>
<label for="google">Google</label>
</div>
<div class="control">
<input
type="checkbox"
id="friend"
name="acquisition"
value="friend"
formControlName="1"
/>
<label for="friend">Referred by friend</label>
</div>
<div class="control">
<input
type="checkbox"
id="other"
name="acquisition"
value="other"
formControlName="2"
/>
<label for="other">Other</label>
</div>
</fieldset>
<div class="control-row">
<div class="control">
<label for="terms-and-conditions">
<input
type="checkbox"
id="terms-and-conditions"
name="terms"
formControlName="agree"
/>
I agree to the terms and conditions
</label>
</div>
</div>
<p class="form-actions">
<button type="reset" class="button button-flat" (click)="onReset()">Reset</button>
<button type="submit" class="button">Sign up</button>
</p>
@if(form.touched && form.invalid) {
<p class="control-error">
Invalid form - please check yout input data.
</p>
}
</form>
1. Creating the Form Group
form = new FormGroup({
email: new FormControl("", {
validators: [Validators.email, Validators.required]
}),
// ... other form controls
});
The FormGroup is the foundation of our form, containing all form controls and nested groups.
2. Nested Form Groups
Our form uses nested groups for better organization:
passwords: new FormGroup({
password: new FormControl(""),
confirmPassword: new FormControl("")
}, {
validators: [equalValues('password', 'confirmPassword')]
}),
address: new FormGroup({
street: new FormControl(""),
number: new FormControl(""),
postalCode: new FormControl(""),
city: new FormControl("")
})
3. Custom Validation
We’ve implemented a custom validator for password matching:
function equalValues(controlName1: string, controlName2: string) {
return (control: AbstractControl) => {
const val1 = control.get(controlName1)?.value
const val2 = control.get(controlName2)?.value
return val1 === val2 ? null : {valueNotEqual: true}
}
}
4. Form Arrays
For multiple checkbox selections, we use FormArray:
source: new FormArray([
new FormControl(false),
new FormControl(false),
new FormControl(false)
])
5. Type Safety
The form demonstrates type safety using TypeScript:
role: new FormControl
'student' | 'teacher' | 'employee' | 'founder' | 'other'
>('student')
Template Integration
The template binds to our reactive form using several key directives:
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="email" type="email">
<!-- Nested form group -->
<div formGroupName="address">
<input formControlName="street">
<!-- ... -->
</div>
<!-- Form array -->
<div formArrayName="source">
<!-- ... -->
</div>
</form>
Form Handling
1. Form Submission
onSubmit() {
if(this.form.invalid){
console.log('INVALID FORM')
return;
}
console.log(this.form)
}
Form Reset
onReset() {
this.form.reset()
}
Form Validation Features
- Built-in Validators:
- Required fields:
Validators.required - Email validation:
Validators.email - Minimum length:
Validators.minLength(6)
- Required fields:
- Custom Validation:
- Password matching
- Group-level validation
- Validation Feedback:
@if(form.touched && form.invalid) {
<p class="control-error">
Invalid form - please check your input data.
</p>
}