R

Reactive Forms

Type-safe Angular reactive forms with validation, custom validators, and form arrays.

Code

typescript
1// registration-form.component.ts
2import { Component, OnInit } from "@angular/core";
3import {
4 FormBuilder,
5 FormGroup,
6 FormArray,
7 Validators,
8 AbstractControl,
9 ValidationErrors,
10 ReactiveFormsModule,
11} from "@angular/forms";
12import { CommonModule } from "@angular/common";
13
14// Form value interface
15interface RegistrationForm {
16 personalInfo: {
17 firstName: string;
18 lastName: string;
19 email: string;
20 };
21 credentials: {
22 password: string;
23 confirmPassword: string;
24 };
25 addresses: Array<{
26 street: string;
27 city: string;
28 zipCode: string;
29 isPrimary: boolean;
30 }>;
31 preferences: {
32 newsletter: boolean;
33 notifications: ("email" | "sms" | "push")[];
34 };
35}
36
37// Custom validators
38function passwordMatchValidator(control: AbstractControl): ValidationErrors | null {
39 const password = control.get("password");
40 const confirmPassword = control.get("confirmPassword");
41
42 if (password && confirmPassword && password.value !== confirmPassword.value) {
43 return { passwordMismatch: true };
44 }
45 return null;
46}
47
48function strongPasswordValidator(control: AbstractControl): ValidationErrors | null {
49 const value = control.value;
50 if (!value) return null;
51
52 const hasUpperCase = /[A-Z]/.test(value);
53 const hasLowerCase = /[a-z]/.test(value);
54 const hasNumber = /[0-9]/.test(value);
55 const hasSpecial = /[!@#$%^&*]/.test(value);
56
57 const valid = hasUpperCase && hasLowerCase && hasNumber && hasSpecial;
58
59 return valid ? null : {
60 strongPassword: {
61 hasUpperCase,
62 hasLowerCase,
63 hasNumber,
64 hasSpecial,
65 },
66 };
67}
68
69@Component({
70 selector: "app-registration-form",
71 standalone: true,
72 imports: [CommonModule, ReactiveFormsModule],
73 template: `
74 <form [formGroup]="form" (ngSubmit)="onSubmit()">
75 <!-- Personal Info -->
76 <fieldset formGroupName="personalInfo">
77 <legend>Personal Information</legend>
78
79 <div class="form-field">
80 <label for="firstName">First Name</label>
81 <input id="firstName" formControlName="firstName" />
82 @if (getError('personalInfo.firstName', 'required')) {
83 <span class="error">First name is required</span>
84 }
85 </div>
86
87 <div class="form-field">
88 <label for="email">Email</label>
89 <input id="email" type="email" formControlName="email" />
90 @if (getError('personalInfo.email', 'email')) {
91 <span class="error">Invalid email format</span>
92 }
93 </div>
94 </fieldset>
95
96 <!-- Credentials -->
97 <fieldset formGroupName="credentials">
98 <legend>Credentials</legend>
99
100 <div class="form-field">
101 <label for="password">Password</label>
102 <input id="password" type="password" formControlName="password" />
103 @if (getError('credentials.password', 'strongPassword')) {
104 <span class="error">Password must contain uppercase, lowercase, number, and special character</span>
105 }
106 </div>
107
108 <div class="form-field">
109 <label for="confirmPassword">Confirm Password</label>
110 <input id="confirmPassword" type="password" formControlName="confirmPassword" />
111 </div>
112
113 @if (form.get('credentials')?.hasError('passwordMismatch')) {
114 <span class="error">Passwords do not match</span>
115 }
116 </fieldset>
117
118 <!-- Addresses (FormArray) -->
119 <fieldset>
120 <legend>Addresses</legend>
121 <div formArrayName="addresses">
122 @for (address of addresses.controls; track $index; let i = $index) {
123 <div [formGroupName]="i" class="address-group">
124 <input formControlName="street" placeholder="Street" />
125 <input formControlName="city" placeholder="City" />
126 <input formControlName="zipCode" placeholder="Zip Code" />
127 <label>
128 <input type="checkbox" formControlName="isPrimary" />
129 Primary
130 </label>
131 <button type="button" (click)="removeAddress(i)">Remove</button>
132 </div>
133 }
134 </div>
135 <button type="button" (click)="addAddress()">Add Address</button>
136 </fieldset>
137
138 <button type="submit" [disabled]="form.invalid">Register</button>
139
140 <pre>{{ form.value | json }}</pre>
141 </form>
142 `,
143})
144export class RegistrationFormComponent implements OnInit {
145 form!: FormGroup;
146
147 constructor(private fb: FormBuilder) {}
148
149 ngOnInit(): void {
150 this.form = this.fb.group({
151 personalInfo: this.fb.group({
152 firstName: ["", [Validators.required, Validators.minLength(2)]],
153 lastName: ["", [Validators.required]],
154 email: ["", [Validators.required, Validators.email]],
155 }),
156 credentials: this.fb.group(
157 {
158 password: ["", [Validators.required, Validators.minLength(8), strongPasswordValidator]],
159 confirmPassword: ["", [Validators.required]],
160 },
161 { validators: passwordMatchValidator }
162 ),
163 addresses: this.fb.array([this.createAddressGroup()]),
164 preferences: this.fb.group({
165 newsletter: [false],
166 notifications: [["email"]],
167 }),
168 });
169 }
170
171 get addresses(): FormArray {
172 return this.form.get("addresses") as FormArray;
173 }
174
175 createAddressGroup(): FormGroup {
176 return this.fb.group({
177 street: ["", Validators.required],
178 city: ["", Validators.required],
179 zipCode: ["", [Validators.required, Validators.pattern(/^\d{5}$/)]],
180 isPrimary: [false],
181 });
182 }
183
184 addAddress(): void {
185 this.addresses.push(this.createAddressGroup());
186 }
187
188 removeAddress(index: number): void {
189 this.addresses.removeAt(index);
190 }
191
192 getError(path: string, error: string): boolean {
193 const control = this.form.get(path);
194 return control ? control.hasError(error) && control.touched : false;
195 }
196
197 onSubmit(): void {
198 if (this.form.valid) {
199 const formValue: RegistrationForm = this.form.value;
200 console.log("Form submitted:", formValue);
201 } else {
202 this.form.markAllAsTouched();
203 }
204 }
205}

Run this example locally

$ ng generate component registration-form