Skip to content

Forms in Angular

Course Objective

Master Angular Forms to build sophisticated user interfaces with validation, dynamic controls, and excellent user experience using both template-driven and reactive approaches.

1. Introduction to Angular Forms

Angular provides two approaches to handling user input through forms:

  • Template-driven forms: Ideal for simple forms with minimal logic
  • Reactive forms: Better for complex forms with dynamic behavior

Key Concepts

  • FormControl: Tracks the value and validation status of an individual control
  • FormGroup: Collection of form controls with validation
  • FormArray: Dynamic array of form controls
  • FormBuilder: Service to create form controls
  • Validators: Functions that validate form inputs

When to Use Each Approach

Template-Driven Reactive
Simple forms Complex forms
Static structure Dynamic structure
Minimal validation Complex validation
Quick prototyping Production applications

2. Template-Driven Forms

Basic Setup

// app.module.ts (Traditional)
import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [FormsModule],
  // ...
})
export class AppModule { }

// main.ts (Standalone)
import { bootstrapApplication } from '@angular/platform-browser';
import { importProvidersFrom } from '@angular/core';
import { FormsModule } from '@angular/forms';

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(FormsModule)
  ]
});

Simple Template-Driven Form

// components/contact-form.component.ts
import { Component } from '@angular/core';
import { NgForm, FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

export interface Contact {
  name: string;
  email: string;
  phone: string;
  message: string;
  category: string;
  subscribe: boolean;
}

@Component({
  selector: 'app-contact-form',
  standalone: true,
  imports: [CommonModule, FormsModule],
  template: `
    <form #contactForm="ngForm" (ngSubmit)="onSubmit(contactForm)" class="contact-form">
      <h2>Contact Us</h2>

      <!-- Name Field -->
      <div class="form-group">
        <label for="name">Name *</label>
        <input
          type="text"
          id="name"
          name="name"
          [(ngModel)]="contact.name"
          #name="ngModel"
          required
          minlength="2"
          class="form-control"
          [class.invalid]="name.invalid && name.touched"
        >
        <div class="error-messages" *ngIf="name.invalid && name.touched">
          <div *ngIf="name.errors?.['required']">Name is required</div>
          <div *ngIf="name.errors?.['minlength']">Name must be at least 2 characters</div>
        </div>
      </div>

      <!-- Email Field -->
      <div class="form-group">
        <label for="email">Email *</label>
        <input
          type="email"
          id="email"
          name="email"
          [(ngModel)]="contact.email"
          #email="ngModel"
          required
          email
          class="form-control"
          [class.invalid]="email.invalid && email.touched"
        >
        <div class="error-messages" *ngIf="email.invalid && email.touched">
          <div *ngIf="email.errors?.['required']">Email is required</div>
          <div *ngIf="email.errors?.['email']">Please enter a valid email</div>
        </div>
      </div>

      <!-- Phone Field -->
      <div class="form-group">
        <label for="phone">Phone</label>
        <input
          type="tel"
          id="phone"
          name="phone"
          [(ngModel)]="contact.phone"
          #phone="ngModel"
          pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"
          class="form-control"
          [class.invalid]="phone.invalid && phone.touched"
          placeholder="123-456-7890"
        >
        <div class="error-messages" *ngIf="phone.invalid && phone.touched">
          <div *ngIf="phone.errors?.['pattern']">Please enter phone in format: 123-456-7890</div>
        </div>
      </div>

      <!-- Category Dropdown -->
      <div class="form-group">
        <label for="category">Category *</label>
        <select
          id="category"
          name="category"
          [(ngModel)]="contact.category"
          #category="ngModel"
          required
          class="form-control"
          [class.invalid]="category.invalid && category.touched"
        >
          <option value="">Select a category</option>
          <option value="general">General Inquiry</option>
          <option value="support">Support</option>
          <option value="sales">Sales</option>
          <option value="feedback">Feedback</option>
        </select>
        <div class="error-messages" *ngIf="category.invalid && category.touched">
          <div *ngIf="category.errors?.['required']">Please select a category</div>
        </div>
      </div>

      <!-- Message Textarea -->
      <div class="form-group">
        <label for="message">Message *</label>
        <textarea
          id="message"
          name="message"
          [(ngModel)]="contact.message"
          #message="ngModel"
          required
          minlength="10"
          maxlength="500"
          rows="4"
          class="form-control"
          [class.invalid]="message.invalid && message.touched"
          placeholder="Please describe your inquiry..."
        ></textarea>
        <div class="char-count">
          {{ contact.message.length }} / 500 characters
        </div>
        <div class="error-messages" *ngIf="message.invalid && message.touched">
          <div *ngIf="message.errors?.['required']">Message is required</div>
          <div *ngIf="message.errors?.['minlength']">Message must be at least 10 characters</div>
        </div>
      </div>

      <!-- Checkbox -->
      <div class="form-group">
        <label class="checkbox-label">
          <input
            type="checkbox"
            name="subscribe"
            [(ngModel)]="contact.subscribe"
          >
          Subscribe to newsletter
        </label>
      </div>

      <!-- Form Status Display -->
      <div class="form-status" *ngIf="contactForm.dirty">
        <p>Form Status:
          <span [class]="contactForm.valid ? 'valid' : 'invalid'">
            {{ contactForm.valid ? 'Valid' : 'Invalid' }}
          </span>
        </p>
      </div>

      <!-- Submit Button -->
      <div class="form-actions">
        <button
          type="submit"
          [disabled]="contactForm.invalid || isSubmitting"
          class="btn btn-primary"
        >
          {{ isSubmitting ? 'Submitting...' : 'Send Message' }}
        </button>
        <button
          type="button"
          (click)="resetForm(contactForm)"
          class="btn btn-secondary"
        >
          Reset
        </button>
      </div>
    </form>
  `,
  styles: [`
    .contact-form {
      max-width: 600px;
      margin: 0 auto;
      padding: 2rem;
      border: 1px solid #ddd;
      border-radius: 8px;
    }

    .form-group {
      margin-bottom: 1.5rem;
    }

    label {
      display: block;
      margin-bottom: 0.5rem;
      font-weight: bold;
    }

    .form-control {
      width: 100%;
      padding: 0.75rem;
      border: 1px solid #ccc;
      border-radius: 4px;
      font-size: 1rem;
    }

    .form-control:focus {
      border-color: #007bff;
      outline: none;
      box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
    }

    .form-control.invalid {
      border-color: #dc3545;
    }

    .error-messages {
      margin-top: 0.5rem;
      color: #dc3545;
      font-size: 0.875rem;
    }

    .char-count {
      margin-top: 0.25rem;
      font-size: 0.875rem;
      color: #666;
      text-align: right;
    }

    .checkbox-label {
      display: flex;
      align-items: center;
      gap: 0.5rem;
      font-weight: normal;
    }

    .form-status .valid {
      color: #28a745;
    }

    .form-status .invalid {
      color: #dc3545;
    }

    .form-actions {
      display: flex;
      gap: 1rem;
      margin-top: 2rem;
    }

    .btn {
      padding: 0.75rem 1.5rem;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 1rem;
      transition: background-color 0.3s;
    }

    .btn-primary {
      background-color: #007bff;
      color: white;
    }

    .btn-primary:hover:not(:disabled) {
      background-color: #0056b3;
    }

    .btn-primary:disabled {
      background-color: #6c757d;
      cursor: not-allowed;
    }

    .btn-secondary {
      background-color: #6c757d;
      color: white;
    }

    .btn-secondary:hover {
      background-color: #545b62;
    }
  `]
})
export class ContactFormComponent {
  contact: Contact = {
    name: '',
    email: '',
    phone: '',
    message: '',
    category: '',
    subscribe: false
  };

  isSubmitting = false;

  onSubmit(form: NgForm): void {
    if (form.valid) {
      this.isSubmitting = true;

      // Simulate API call
      setTimeout(() => {
        console.log('Contact form submitted:', this.contact);
        alert('Thank you for your message! We will get back to you soon.');
        this.resetForm(form);
        this.isSubmitting = false;
      }, 2000);
    } else {
      console.log('Form is invalid');
      this.markAllFieldsAsTouched(form);
    }
  }

  resetForm(form: NgForm): void {
    form.resetForm();
    this.contact = {
      name: '',
      email: '',
      phone: '',
      message: '',
      category: '',
      subscribe: false
    };
  }

  private markAllFieldsAsTouched(form: NgForm): void {
    Object.keys(form.controls).forEach(key => {
      form.controls[key].markAsTouched();
    });
  }
}

3. Reactive Forms

Basic Setup

// app.module.ts (Traditional)
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  imports: [ReactiveFormsModule],
  // ...
})
export class AppModule { }

// main.ts (Standalone)
import { importProvidersFrom } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(ReactiveFormsModule)
  ]
});

Comprehensive Reactive Form

// components/user-registration.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import {
  FormBuilder,
  FormGroup,
  FormControl,
  Validators,
  AbstractControl,
  ReactiveFormsModule
} from '@angular/forms';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { takeUntil, debounceTime, distinctUntilChanged } from 'rxjs/operators';

@Component({
  selector: 'app-user-registration',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  template: `
    <form [formGroup]="registrationForm" (ngSubmit)="onSubmit()" class="registration-form">
      <h2>Create Account</h2>

      <!-- Personal Information Section -->
      <fieldset>
        <legend>Personal Information</legend>

        <div class="form-row">
          <div class="form-group">
            <label for="firstName">First Name *</label>
            <input
              type="text"
              id="firstName"
              formControlName="firstName"
              class="form-control"
              [class.invalid]="isFieldInvalid('firstName')"
            >
            <div class="error-messages" *ngIf="isFieldInvalid('firstName')">
              <div *ngIf="getFieldError('firstName', 'required')">First name is required</div>
              <div *ngIf="getFieldError('firstName', 'minlength')">
                First name must be at least 2 characters
              </div>
            </div>
          </div>

          <div class="form-group">
            <label for="lastName">Last Name *</label>
            <input
              type="text"
              id="lastName"
              formControlName="lastName"
              class="form-control"
              [class.invalid]="isFieldInvalid('lastName')"
            >
            <div class="error-messages" *ngIf="isFieldInvalid('lastName')">
              <div *ngIf="getFieldError('lastName', 'required')">Last name is required</div>
            </div>
          </div>
        </div>

        <div class="form-group">
          <label for="email">Email *</label>
          <input
            type="email"
            id="email"
            formControlName="email"
            class="form-control"
            [class.invalid]="isFieldInvalid('email')"
          >
          <div class="error-messages" *ngIf="isFieldInvalid('email')">
            <div *ngIf="getFieldError('email', 'required')">Email is required</div>
            <div *ngIf="getFieldError('email', 'email')">Please enter a valid email</div>
            <div *ngIf="getFieldError('email', 'emailTaken')">This email is already registered</div>
          </div>
          <div class="field-status" *ngIf="emailCheckStatus">
            {{ emailCheckStatus }}
          </div>
        </div>

        <div class="form-group">
          <label for="phone">Phone</label>
          <input
            type="tel"
            id="phone"
            formControlName="phone"
            class="form-control"
            [class.invalid]="isFieldInvalid('phone')"
            placeholder="(555) 123-4567"
          >
          <div class="error-messages" *ngIf="isFieldInvalid('phone')">
            <div *ngIf="getFieldError('phone', 'pattern')">
              Please enter a valid phone number
            </div>
          </div>
        </div>

        <div class="form-group">
          <label for="birthDate">Date of Birth</label>
          <input
            type="date"
            id="birthDate"
            formControlName="birthDate"
            class="form-control"
            [class.invalid]="isFieldInvalid('birthDate')"
          >
          <div class="error-messages" *ngIf="isFieldInvalid('birthDate')">
            <div *ngIf="getFieldError('birthDate', 'minimumAge')">
              You must be at least 18 years old
            </div>
          </div>
        </div>
      </fieldset>

      <!-- Account Information Section -->
      <fieldset>
        <legend>Account Information</legend>

        <div class="form-group">
          <label for="username">Username *</label>
          <input
            type="text"
            id="username"
            formControlName="username"
            class="form-control"
            [class.invalid]="isFieldInvalid('username')"
          >
          <div class="error-messages" *ngIf="isFieldInvalid('username')">
            <div *ngIf="getFieldError('username', 'required')">Username is required</div>
            <div *ngIf="getFieldError('username', 'minlength')">
              Username must be at least 3 characters
            </div>
            <div *ngIf="getFieldError('username', 'pattern')">
              Username can only contain letters, numbers, and underscores
            </div>
          </div>
        </div>

        <div class="form-group">
          <label for="password">Password *</label>
          <input
            type="password"
            id="password"
            formControlName="password"
            class="form-control"
            [class.invalid]="isFieldInvalid('password')"
          >
          <div class="password-strength">
            <div class="strength-indicator">
              <div
                class="strength-bar"
                [class]="passwordStrength"
                [style.width.%]="passwordStrengthWidth"
              ></div>
            </div>
            <span class="strength-text">{{ passwordStrengthText }}</span>
          </div>
          <div class="error-messages" *ngIf="isFieldInvalid('password')">
            <div *ngIf="getFieldError('password', 'required')">Password is required</div>
            <div *ngIf="getFieldError('password', 'minlength')">
              Password must be at least 8 characters
            </div>
            <div *ngIf="getFieldError('password', 'pattern')">
              Password must contain at least one uppercase letter, one lowercase letter, and one number
            </div>
          </div>
        </div>

        <div class="form-group">
          <label for="confirmPassword">Confirm Password *</label>
          <input
            type="password"
            id="confirmPassword"
            formControlName="confirmPassword"
            class="form-control"
            [class.invalid]="isFieldInvalid('confirmPassword')"
          >
          <div class="error-messages" *ngIf="isFieldInvalid('confirmPassword')">
            <div *ngIf="getFieldError('confirmPassword', 'required')">
              Please confirm your password
            </div>
            <div *ngIf="getFieldError('confirmPassword', 'passwordMismatch')">
              Passwords do not match
            </div>
          </div>
        </div>
      </fieldset>

      <!-- Preferences Section -->
      <fieldset>
        <legend>Preferences</legend>

        <div class="form-group">
          <label class="checkbox-label">
            <input type="checkbox" formControlName="agreeToTerms">
            I agree to the <a href="/terms" target="_blank">Terms of Service</a> *
          </label>
          <div class="error-messages" *ngIf="isFieldInvalid('agreeToTerms')">
            <div *ngIf="getFieldError('agreeToTerms', 'required')">
              You must agree to the terms of service
            </div>
          </div>
        </div>

        <div class="form-group">
          <label class="checkbox-label">
            <input type="checkbox" formControlName="subscribeToNewsletter">
            Subscribe to newsletter
          </label>
        </div>
      </fieldset>

      <!-- Form Actions -->
      <div class="form-actions">
        <button
          type="submit"
          [disabled]="registrationForm.invalid || isSubmitting"
          class="btn btn-primary"
        >
          {{ isSubmitting ? 'Creating Account...' : 'Create Account' }}
        </button>
        <button
          type="button"
          (click)="resetForm()"
          class="btn btn-secondary"
        >
          Reset
        </button>
      </div>

      <!-- Debug Information (Development Only) -->
      <div class="debug-info" *ngIf="showDebugInfo">
        <h4>Form Debug Info</h4>
        <p>Form Valid: {{ registrationForm.valid }}</p>
        <p>Form Value: {{ registrationForm.value | json }}</p>
        <p>Form Errors: {{ getFormErrors() | json }}</p>
      </div>
    </form>
  `,
  styles: [`
    .registration-form {
      max-width: 800px;
      margin: 0 auto;
      padding: 2rem;
    }

    fieldset {
      border: 1px solid #ddd;
      border-radius: 8px;
      padding: 1.5rem;
      margin-bottom: 2rem;
    }

    legend {
      font-weight: bold;
      font-size: 1.2rem;
      padding: 0 0.5rem;
    }

    .form-row {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 1rem;
    }

    .form-group {
      margin-bottom: 1.5rem;
    }

    label {
      display: block;
      margin-bottom: 0.5rem;
      font-weight: bold;
    }

    .form-control {
      width: 100%;
      padding: 0.75rem;
      border: 1px solid #ccc;
      border-radius: 4px;
      font-size: 1rem;
    }

    .form-control:focus {
      border-color: #007bff;
      outline: none;
      box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
    }

    .form-control.invalid {
      border-color: #dc3545;
    }

    .error-messages {
      margin-top: 0.5rem;
      color: #dc3545;
      font-size: 0.875rem;
    }

    .field-status {
      margin-top: 0.5rem;
      font-size: 0.875rem;
      color: #007bff;
    }

    .password-strength {
      margin-top: 0.5rem;
    }

    .strength-indicator {
      height: 4px;
      background-color: #e9ecef;
      border-radius: 2px;
      overflow: hidden;
    }

    .strength-bar {
      height: 100%;
      transition: width 0.3s, background-color 0.3s;
    }

    .strength-bar.weak { background-color: #dc3545; }
    .strength-bar.fair { background-color: #ffc107; }
    .strength-bar.good { background-color: #17a2b8; }
    .strength-bar.strong { background-color: #28a745; }

    .strength-text {
      font-size: 0.875rem;
      margin-left: 0.5rem;
    }

    .checkbox-label {
      display: flex;
      align-items: flex-start;
      gap: 0.5rem;
      font-weight: normal;
    }

    .form-actions {
      display: flex;
      gap: 1rem;
      margin-top: 2rem;
    }

    .btn {
      padding: 0.75rem 1.5rem;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 1rem;
      transition: background-color 0.3s;
    }

    .btn-primary {
      background-color: #007bff;
      color: white;
    }

    .btn-primary:hover:not(:disabled) {
      background-color: #0056b3;
    }

    .btn-primary:disabled {
      background-color: #6c757d;
      cursor: not-allowed;
    }

    .btn-secondary {
      background-color: #6c757d;
      color: white;
    }

    .btn-secondary:hover {
      background-color: #545b62;
    }

    .debug-info {
      margin-top: 2rem;
      padding: 1rem;
      background-color: #f8f9fa;
      border-radius: 4px;
      font-family: monospace;
      font-size: 0.875rem;
    }

    @media (max-width: 768px) {
      .form-row {
        grid-template-columns: 1fr;
      }
    }
  `]
})
export class UserRegistrationComponent implements OnInit, OnDestroy {
  registrationForm: FormGroup;
  isSubmitting = false;
  emailCheckStatus = '';
  passwordStrength = 'weak';
  passwordStrengthWidth = 0;
  passwordStrengthText = '';
  showDebugInfo = false; // Set to true for development

  private destroy$ = new Subject<void>();

  constructor(private fb: FormBuilder) {
    this.registrationForm = this.createForm();
  }

  ngOnInit(): void {
    this.setupFormListeners();
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  private createForm(): FormGroup {
    return this.fb.group({
      firstName: ['', [Validators.required, Validators.minLength(2)]],
      lastName: ['', [Validators.required]],
      email: ['', [Validators.required, Validators.email], [this.emailExistsValidator.bind(this)]],
      phone: ['', [Validators.pattern(/^\(\d{3}\) \d{3}-\d{4}$/)]],
      birthDate: ['', [this.minimumAgeValidator(18)]],
      username: ['', [
        Validators.required,
        Validators.minLength(3),
        Validators.pattern(/^[a-zA-Z0-9_]+$/)
      ]],
      password: ['', [
        Validators.required,
        Validators.minLength(8),
        Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
      ]],
      confirmPassword: ['', [Validators.required]],
      agreeToTerms: [false, [Validators.requiredTrue]],
      subscribeToNewsletter: [false]
    }, {
      validators: [this.passwordMatchValidator]
    });
  }

  private setupFormListeners(): void {
    // Email existence check
    this.registrationForm.get('email')?.valueChanges
      .pipe(
        takeUntil(this.destroy$),
        debounceTime(500),
        distinctUntilChanged()
      )
      .subscribe(email => {
        if (email && this.registrationForm.get('email')?.valid) {
          this.emailCheckStatus = 'Checking availability...';
        }
      });

    // Password strength check
    this.registrationForm.get('password')?.valueChanges
      .pipe(takeUntil(this.destroy$))
      .subscribe(password => {
        this.updatePasswordStrength(password);
      });
  }

  // Custom Validators
  private emailExistsValidator(control: AbstractControl): Promise<any> {
    if (!control.value) {
      return Promise.resolve(null);
    }

    return new Promise((resolve) => {
      // Simulate API call
      setTimeout(() => {
        const existingEmails = ['test@example.com', 'admin@example.com'];
        if (existingEmails.includes(control.value)) {
          this.emailCheckStatus = 'Email is already taken';
          resolve({ emailTaken: true });
        } else {
          this.emailCheckStatus = 'Email is available';
          resolve(null);
        }
      }, 1000);
    });
  }

  private minimumAgeValidator(minAge: number) {
    return (control: AbstractControl) => {
      if (!control.value) {
        return null;
      }

      const birthDate = new Date(control.value);
      const today = new Date();
      const age = today.getFullYear() - birthDate.getFullYear();
      const monthDiff = today.getMonth() - birthDate.getMonth();

      if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
        age--;
      }

      return age >= minAge ? null : { minimumAge: { requiredAge: minAge, actualAge: age } };
    };
  }

  private passwordMatchValidator(group: AbstractControl) {
    const password = group.get('password');
    const confirmPassword = group.get('confirmPassword');

    if (!password || !confirmPassword) {
      return null;
    }

    return password.value === confirmPassword.value ? null : { passwordMismatch: true };
  }

  private updatePasswordStrength(password: string): void {
    if (!password) {
      this.passwordStrength = 'weak';
      this.passwordStrengthWidth = 0;
      this.passwordStrengthText = '';
      return;
    }

    let score = 0;
    if (password.length >= 8) score++;
    if (/[a-z]/.test(password)) score++;
    if (/[A-Z]/.test(password)) score++;
    if (/\d/.test(password)) score++;
    if (/[^a-zA-Z\d]/.test(password)) score++;

    switch (score) {
      case 0:
      case 1:
        this.passwordStrength = 'weak';
        this.passwordStrengthWidth = 25;
        this.passwordStrengthText = 'Weak';
        break;
      case 2:
      case 3:
        this.passwordStrength = 'fair';
        this.passwordStrengthWidth = 50;
        this.passwordStrengthText = 'Fair';
        break;
      case 4:
        this.passwordStrength = 'good';
        this.passwordStrengthWidth = 75;
        this.passwordStrengthText = 'Good';
        break;
      case 5:
        this.passwordStrength = 'strong';
        this.passwordStrengthWidth = 100;
        this.passwordStrengthText = 'Strong';
        break;
    }
  }

  // Utility Methods
  isFieldInvalid(fieldName: string): boolean {
    const field = this.registrationForm.get(fieldName);
    return field ? field.invalid && (field.dirty || field.touched) : false;
  }

  getFieldError(fieldName: string, errorType: string): boolean {
    const field = this.registrationForm.get(fieldName);
    return field ? field.hasError(errorType) : false;
  }

  getFormErrors(): any {
    const formErrors: any = {};

    Object.keys(this.registrationForm.controls).forEach(key => {
      const controlErrors = this.registrationForm.get(key)?.errors;
      if (controlErrors) {
        formErrors[key] = controlErrors;
      }
    });

    return formErrors;
  }

  onSubmit(): void {
    if (this.registrationForm.valid) {
      this.isSubmitting = true;

      const formData = this.registrationForm.value;
      delete formData.confirmPassword; // Remove confirm password from submission

      // Simulate API call
      setTimeout(() => {
        console.log('Registration data:', formData);
        alert('Account created successfully!');
        this.resetForm();
        this.isSubmitting = false;
      }, 2000);
    } else {
      this.markAllFieldsAsTouched();
    }
  }

  resetForm(): void {
    this.registrationForm.reset();
    this.emailCheckStatus = '';
    this.passwordStrength = 'weak';
    this.passwordStrengthWidth = 0;
    this.passwordStrengthText = '';
  }

  private markAllFieldsAsTouched(): void {
    Object.keys(this.registrationForm.controls).forEach(key => {
      this.registrationForm.get(key)?.markAsTouched();
    });
  }
}

4. Form Validation

Built-in Validators

import { Validators, AbstractControl, ValidationErrors } from '@angular/forms';

// Common built-in validators
export class FormValidationExamples {
  createFormWithValidators() {
    return this.fb.group({
      // Required field
      name: ['', Validators.required],

      // Email validation
      email: ['', [Validators.required, Validators.email]],

      // Length validation
      username: ['', [
        Validators.required,
        Validators.minLength(3),
        Validators.maxLength(20)
      ]],

      // Pattern validation
      phone: ['', Validators.pattern(/^\d{3}-\d{3}-\d{4}$/)],

      // Numeric validation
      age: ['', [
        Validators.required,
        Validators.min(18),
        Validators.max(120)
      ]],

      // Multiple validators
      password: ['', [
        Validators.required,
        Validators.minLength(8),
        Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
      ]]
    });
  }
}

Error Message Component

// components/error-message.component.ts
import { Component, Input } from '@angular/core';
import { AbstractControl } from '@angular/forms';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-error-message',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="error-messages" *ngIf="shouldShowError">
      <div *ngFor="let error of errorMessages">
        {{ error }}
      </div>
    </div>
  `,
  styles: [`
    .error-messages {
      margin-top: 0.5rem;
      color: #dc3545;
      font-size: 0.875rem;
    }
  `]
})
export class ErrorMessageComponent {
  @Input() control: AbstractControl | null = null;
  @Input() fieldName = '';

  get shouldShowError(): boolean {
    return !!(this.control && this.control.invalid && (this.control.dirty || this.control.touched));
  }

  get errorMessages(): string[] {
    if (!this.control || !this.control.errors) {
      return [];
    }

    const messages: string[] = [];
    const errors = this.control.errors;

    if (errors['required']) {
      messages.push(`${this.fieldName} is required`);
    }

    if (errors['email']) {
      messages.push('Please enter a valid email address');
    }

    if (errors['minlength']) {
      const requiredLength = errors['minlength'].requiredLength;
      messages.push(`${this.fieldName} must be at least ${requiredLength} characters`);
    }

    if (errors['maxlength']) {
      const requiredLength = errors['maxlength'].requiredLength;
      messages.push(`${this.fieldName} cannot exceed ${requiredLength} characters`);
    }

    if (errors['pattern']) {
      messages.push(`${this.fieldName} format is invalid`);
    }

    if (errors['min']) {
      messages.push(`${this.fieldName} must be at least ${errors['min'].min}`);
    }

    if (errors['max']) {
      messages.push(`${this.fieldName} cannot exceed ${errors['max'].max}`);
    }

    return messages;
  }
}

// Usage in forms
/*
<input formControlName="email" class="form-control">
<app-error-message [control]="form.get('email')" fieldName="Email"></app-error-message>
*/

5. Custom Validators

Sync Custom Validators

// validators/custom-validators.ts
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

export class CustomValidators {
  // Password strength validator
  static passwordStrength(control: AbstractControl): ValidationErrors | null {
    const value = control.value;
    if (!value) {
      return null;
    }

    const hasUpperCase = /[A-Z]/.test(value);
    const hasLowerCase = /[a-z]/.test(value);
    const hasNumeric = /[0-9]/.test(value);
    const hasSpecialChar = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(value);

    const passwordValid = hasUpperCase && hasLowerCase && hasNumeric && hasSpecialChar;

    if (!passwordValid) {
      return {
        passwordStrength: {
          hasUpperCase,
          hasLowerCase,
          hasNumeric,
          hasSpecialChar
        }
      };
    }

    return null;
  }

  // Age range validator
  static ageRange(min: number, max: number): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      if (!control.value) {
        return null;
      }

      const age = parseInt(control.value, 10);
      if (isNaN(age) || age < min || age > max) {
        return {
          ageRange: {
            min,
            max,
            actual: age
          }
        };
      }

      return null;
    };
  }

  // Credit card validator
  static creditCard(control: AbstractControl): ValidationErrors | null {
    if (!control.value) {
      return null;
    }

    const value = control.value.replace(/\s/g, '');

    // Luhn algorithm
    let sum = 0;
    let isEven = false;

    for (let i = value.length - 1; i >= 0; i--) {
      let digit = parseInt(value.charAt(i), 10);

      if (isEven) {
        digit *= 2;
        if (digit > 9) {
          digit -= 9;
        }
      }

      sum += digit;
      isEven = !isEven;
    }

    return sum % 10 === 0 ? null : { creditCard: true };
  }

  // URL validator
  static url(control: AbstractControl): ValidationErrors | null {
    if (!control.value) {
      return null;
    }

    try {
      new URL(control.value);
      return null;
    } catch {
      return { url: true };
    }
  }

  // No whitespace validator
  static noWhitespace(control: AbstractControl): ValidationErrors | null {
    if (!control.value) {
      return null;
    }

    const isWhitespace = (control.value || '').trim().length === 0;
    return isWhitespace ? { whitespace: true } : null;
  }

  // Date range validator
  static dateRange(startDate: Date, endDate: Date): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      if (!control.value) {
        return null;
      }

      const inputDate = new Date(control.value);

      if (inputDate < startDate || inputDate > endDate) {
        return {
          dateRange: {
            min: startDate,
            max: endDate,
            actual: inputDate
          }
        };
      }

      return null;
    };
  }
}

Async Custom Validators

// validators/async-validators.ts
import { Injectable } from '@angular/core';
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, of, timer } from 'rxjs';
import { map, switchMap, catchError } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class AsyncValidators {
  constructor(private http: HttpClient) {}

  // Check if username is available
  usernameAvailable(): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors | null> => {
      if (!control.value) {
        return of(null);
      }

      return timer(500).pipe( // Debounce
        switchMap(() =>
          this.http.get<{available: boolean}>(`/api/check-username/${control.value}`)
        ),
        map(response => response.available ? null : { usernameTaken: true }),
        catchError(() => of(null)) // Return null on error
      );
    };
  }

  // Email existence validator
  emailExists(): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors | null> => {
      if (!control.value) {
        return of(null);
      }

      return timer(300).pipe(
        switchMap(() =>
          this.http.post<{exists: boolean}>('/api/check-email', { email: control.value })
        ),
        map(response => response.exists ? { emailExists: true } : null),
        catchError(() => of(null))
      );
    };
  }

  // Domain validation (check if domain exists)
  domainValidator(): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors | null> => {
      if (!control.value) {
        return of(null);
      }

      try {
        const url = new URL(control.value);
        const domain = url.hostname;

        return this.http.get(`https://dns.google/resolve?name=${domain}&type=A`).pipe(
          map(() => null), // Domain exists
          catchError(() => of({ invalidDomain: true }))
        );
      } catch {
        return of({ invalidUrl: true });
      }
    };
  }
}

6. Dynamic Forms

Dynamic Form Builder

// models/form-config.interface.ts
export interface FormFieldConfig {
  type: 'text' | 'email' | 'password' | 'number' | 'select' | 'checkbox' | 'textarea' | 'date';
  key: string;
  label: string;
  placeholder?: string;
  required?: boolean;
  disabled?: boolean;
  options?: { value: any; label: string; }[];
  validators?: any[];
  asyncValidators?: any[];
  cssClass?: string;
  description?: string;
  group?: string;
}

export interface FormConfig {
  title: string;
  description?: string;
  fields: FormFieldConfig[];
  submitButtonText?: string;
}
// components/dynamic-form.component.ts
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { FormConfig, FormFieldConfig } from '../models/form-config.interface';

@Component({
  selector: 'app-dynamic-form',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  template: `
    <form [formGroup]="dynamicForm" (ngSubmit)="onSubmit()" class="dynamic-form">
      <h2 *ngIf="config.title">{{ config.title }}</h2>
      <p *ngIf="config.description" class="form-description">{{ config.description }}</p>

      <div *ngFor="let group of fieldGroups; trackBy: trackByGroup" class="field-group">
        <h3 *ngIf="group.name" class="group-title">{{ group.name }}</h3>

        <div *ngFor="let field of group.fields; trackBy: trackByField"
             class="form-field"
             [ngClass]="field.cssClass">

          <!-- Text Input -->
          <div *ngIf="field.type === 'text' || field.type === 'email' || field.type === 'password'"
               class="form-group">
            <label [for]="field.key">
              {{ field.label }}
              <span *ngIf="field.required" class="required">*</span>
            </label>
            <input
              [id]="field.key"
              [type]="field.type"
              [formControlName]="field.key"
              [placeholder]="field.placeholder || ''"
              class="form-control"
              [class.invalid]="isFieldInvalid(field.key)"
            >
            <small *ngIf="field.description" class="field-description">
              {{ field.description }}
            </small>
            <app-error-message
              [control]="dynamicForm.get(field.key)"
              [fieldName]="field.label">
            </app-error-message>
          </div>

          <!-- Number Input -->
          <div *ngIf="field.type === 'number'" class="form-group">
            <label [for]="field.key">
              {{ field.label }}
              <span *ngIf="field.required" class="required">*</span>
            </label>
            <input
              [id]="field.key"
              type="number"
              [formControlName]="field.key"
              [placeholder]="field.placeholder || ''"
              class="form-control"
              [class.invalid]="isFieldInvalid(field.key)"
            >
            <small *ngIf="field.description" class="field-description">
              {{ field.description }}
            </small>
            <app-error-message
              [control]="dynamicForm.get(field.key)"
              [fieldName]="field.label">
            </app-error-message>
          </div>

          <!-- Date Input -->
          <div *ngIf="field.type === 'date'" class="form-group">
            <label [for]="field.key">
              {{ field.label }}
              <span *ngIf="field.required" class="required">*</span>
            </label>
            <input
              [id]="field.key"
              type="date"
              [formControlName]="field.key"
              class="form-control"
              [class.invalid]="isFieldInvalid(field.key)"
            >
            <small *ngIf="field.description" class="field-description">
              {{ field.description }}
            </small>
            <app-error-message
              [control]="dynamicForm.get(field.key)"
              [fieldName]="field.label">
            </app-error-message>
          </div>

          <!-- Select Dropdown -->
          <div *ngIf="field.type === 'select'" class="form-group">
            <label [for]="field.key">
              {{ field.label }}
              <span *ngIf="field.required" class="required">*</span>
            </label>
            <select
              [id]="field.key"
              [formControlName]="field.key"
              class="form-control"
              [class.invalid]="isFieldInvalid(field.key)"
            >
              <option value="">{{ field.placeholder || 'Select an option' }}</option>
              <option *ngFor="let option of field.options" [value]="option.value">
                {{ option.label }}
              </option>
            </select>
            <small *ngIf="field.description" class="field-description">
              {{ field.description }}
            </small>
            <app-error-message
              [control]="dynamicForm.get(field.key)"
              [fieldName]="field.label">
            </app-error-message>
          </div>

          <!-- Textarea -->
          <div *ngIf="field.type === 'textarea'" class="form-group">
            <label [for]="field.key">
              {{ field.label }}
              <span *ngIf="field.required" class="required">*</span>
            </label>
            <textarea
              [id]="field.key"
              [formControlName]="field.key"
              [placeholder]="field.placeholder || ''"
              rows="4"
              class="form-control"
              [class.invalid]="isFieldInvalid(field.key)"
            ></textarea>
            <small *ngIf="field.description" class="field-description">
              {{ field.description }}
            </small>
            <app-error-message
              [control]="dynamicForm.get(field.key)"
              [fieldName]="field.label">
            </app-error-message>
          </div>

          <!-- Checkbox -->
          <div *ngIf="field.type === 'checkbox'" class="form-group">
            <label class="checkbox-label">
              <input
                [id]="field.key"
                type="checkbox"
                [formControlName]="field.key"
              >
              {{ field.label }}
              <span *ngIf="field.required" class="required">*</span>
            </label>
            <small *ngIf="field.description" class="field-description">
              {{ field.description }}
            </small>
            <app-error-message
              [control]="dynamicForm.get(field.key)"
              [fieldName]="field.label">
            </app-error-message>
          </div>
        </div>
      </div>

      <div class="form-actions">
        <button
          type="submit"
          [disabled]="dynamicForm.invalid || isSubmitting"
          class="btn btn-primary"
        >
          {{ isSubmitting ? 'Submitting...' : (config.submitButtonText || 'Submit') }}
        </button>
        <button
          type="button"
          (click)="resetForm()"
          class="btn btn-secondary"
        >
          Reset
        </button>
      </div>
    </form>
  `,
  styles: [`
    .dynamic-form {
      max-width: 800px;
      margin: 0 auto;
      padding: 2rem;
    }

    .form-description {
      margin-bottom: 2rem;
      color: #666;
    }

    .field-group {
      margin-bottom: 2rem;
      padding: 1.5rem;
      border: 1px solid #e9ecef;
      border-radius: 8px;
    }

    .group-title {
      margin-top: 0;
      margin-bottom: 1.5rem;
      font-size: 1.2rem;
      font-weight: bold;
      color: #333;
    }

    .form-group {
      margin-bottom: 1.5rem;
    }

    label {
      display: block;
      margin-bottom: 0.5rem;
      font-weight: bold;
    }

    .required {
      color: #dc3545;
    }

    .form-control {
      width: 100%;
      padding: 0.75rem;
      border: 1px solid #ccc;
      border-radius: 4px;
      font-size: 1rem;
    }

    .form-control:focus {
      border-color: #007bff;
      outline: none;
      box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
    }

    .form-control.invalid {
      border-color: #dc3545;
    }

    .field-description {
      display: block;
      margin-top: 0.25rem;
      color: #666;
      font-size: 0.875rem;
    }

    .checkbox-label {
      display: flex;
      align-items: center;
      gap: 0.5rem;
      font-weight: normal;
    }

    .form-actions {
      display: flex;
      gap: 1rem;
      margin-top: 2rem;
      padding-top: 2rem;
      border-top: 1px solid #e9ecef;
    }

    .btn {
      padding: 0.75rem 1.5rem;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 1rem;
      transition: background-color 0.3s;
    }

    .btn-primary {
      background-color: #007bff;
      color: white;
    }

    .btn-primary:hover:not(:disabled) {
      background-color: #0056b3;
    }

    .btn-primary:disabled {
      background-color: #6c757d;
      cursor: not-allowed;
    }

    .btn-secondary {
      background-color: #6c757d;
      color: white;
    }

    .btn-secondary:hover {
      background-color: #545b62;
    }
  `]
})
export class DynamicFormComponent implements OnInit {
  @Input() config!: FormConfig;
  @Input() initialData: any = {};
  @Output() formSubmit = new EventEmitter<any>();
  @Output() formChange = new EventEmitter<any>();

  dynamicForm!: FormGroup;
  isSubmitting = false;
  fieldGroups: { name: string; fields: FormFieldConfig[] }[] = [];

  constructor(private fb: FormBuilder) {}

  ngOnInit(): void {
    this.createForm();
    this.organizeFieldsByGroup();
  }

  private createForm(): void {
    const group: any = {};

    this.config.fields.forEach(field => {
      const validators = this.buildValidators(field);
      const asyncValidators = field.asyncValidators || [];

      group[field.key] = [
        { value: this.initialData[field.key] || '', disabled: field.disabled },
        validators,
        asyncValidators
      ];
    });

    this.dynamicForm = this.fb.group(group);

    // Listen to form changes
    this.dynamicForm.valueChanges.subscribe(value => {
      this.formChange.emit(value);
    });
  }

  private buildValidators(field: FormFieldConfig): any[] {
    const validators: any[] = [];

    if (field.required) {
      validators.push(Validators.required);
    }

    if (field.type === 'email') {
      validators.push(Validators.email);
    }

    // Add custom validators from field config
    if (field.validators) {
      validators.push(...field.validators);
    }

    return validators;
  }

  private organizeFieldsByGroup(): void {
    const groups: { [key: string]: FormFieldConfig[] } = {};

    this.config.fields.forEach(field => {
      const groupName = field.group || 'default';
      if (!groups[groupName]) {
        groups[groupName] = [];
      }
      groups[groupName].push(field);
    });

    this.fieldGroups = Object.keys(groups).map(groupName => ({
      name: groupName === 'default' ? '' : groupName,
      fields: groups[groupName]
    }));
  }

  isFieldInvalid(fieldName: string): boolean {
    const field = this.dynamicForm.get(fieldName);
    return field ? field.invalid && (field.dirty || field.touched) : false;
  }

  onSubmit(): void {
    if (this.dynamicForm.valid) {
      this.isSubmitting = true;
      this.formSubmit.emit(this.dynamicForm.value);
    } else {
      this.markAllFieldsAsTouched();
    }
  }

  resetForm(): void {
    this.dynamicForm.reset();
    this.isSubmitting = false;
  }

  private markAllFieldsAsTouched(): void {
    Object.keys(this.dynamicForm.controls).forEach(key => {
      this.dynamicForm.get(key)?.markAsTouched();
    });
  }

  trackByGroup(index: number, group: any): string {
    return group.name;
  }

  trackByField(index: number, field: FormFieldConfig): string {
    return field.key;
  }
}

Using Dynamic Forms

// components/survey.component.ts
import { Component } from '@angular/core';
import { FormConfig } from '../models/form-config.interface';
import { DynamicFormComponent } from './dynamic-form.component';

@Component({
  selector: 'app-survey',
  standalone: true,
  imports: [DynamicFormComponent],
  template: `
    <app-dynamic-form
      [config]="surveyConfig"
      [initialData]="initialData"
      (formSubmit)="onSurveySubmit($event)"
      (formChange)="onFormChange($event)">
    </app-dynamic-form>
  `
})
export class SurveyComponent {
  surveyConfig: FormConfig = {
    title: 'Customer Satisfaction Survey',
    description: 'Please help us improve by sharing your feedback.',
    submitButtonText: 'Submit Survey',
    fields: [
      {
        type: 'text',
        key: 'name',
        label: 'Full Name',
        required: true,
        group: 'Personal Information'
      },
      {
        type: 'email',
        key: 'email',
        label: 'Email Address',
        required: true,
        group: 'Personal Information'
      },
      {
        type: 'select',
        key: 'satisfaction',
        label: 'Overall Satisfaction',
        required: true,
        options: [
          { value: 'very-satisfied', label: 'Very Satisfied' },
          { value: 'satisfied', label: 'Satisfied' },
          { value: 'neutral', label: 'Neutral' },
          { value: 'dissatisfied', label: 'Dissatisfied' },
          { value: 'very-dissatisfied', label: 'Very Dissatisfied' }
        ],
        group: 'Feedback'
      },
      {
        type: 'textarea',
        key: 'comments',
        label: 'Additional Comments',
        placeholder: 'Please share any additional feedback...',
        group: 'Feedback'
      },
      {
        type: 'checkbox',
        key: 'subscribe',
        label: 'Subscribe to our newsletter',
        group: 'Preferences'
      }
    ]
  };

  initialData = {
    name: '',
    email: '',
    satisfaction: '',
    comments: '',
    subscribe: false
  };

  onSurveySubmit(data: any): void {
    console.log('Survey submitted:', data);
    // Handle survey submission
  }

  onFormChange(data: any): void {
    console.log('Form changed:', data);
    // Handle form changes (auto-save, etc.)
  }
}

I'll continue this comprehensive forms document in the next response, covering Form Arrays, Cross-Field Validation, State Management, Testing, and Best Practices. Would you like me to continue with the remaining sections?