> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 07. Angular Forms

> Master template-driven and reactive forms for robust user input handling and validation

<Frame>
  <img src="https://mintcdn.com/devweeekends/AEOaWh79Ur7CdHHv/images/courses/angular-crash-course/forms.svg?fit=max&auto=format&n=AEOaWh79Ur7CdHHv&q=85&s=3a06de90de7ca0723632fb20dc1c0bf6" alt="Angular Forms" width="900" height="550" data-path="images/courses/angular-crash-course/forms.svg" />
</Frame>

## Module Overview

<Info>
  **Estimated Time**: 4-5 hours | **Difficulty**: Intermediate | **Prerequisites**: Module 6
</Info>

Angular provides two approaches to handling forms: template-driven forms for simple cases and reactive forms for complex scenarios. Both provide validation, error handling, and form state management.

Forms are often the most complex part of any frontend application -- they involve user input, validation, error messages, async checks (like "is this username taken?"), conditional fields, and state management. Getting forms right is the difference between an app that feels polished and one that frustrates users.

<Note>
  **Which should you use?** For a contact form or a simple login page, template-driven forms are faster to build. For anything with dynamic fields, multi-step wizards, complex validation logic, or forms you need to unit test thoroughly, use reactive forms. In production codebases, reactive forms are overwhelmingly more common because most real-world forms are more complex than tutorials suggest.
</Note>

**What You'll Learn:**

* Template-driven forms basics
* Reactive forms deep dive
* Built-in and custom validators
* Dynamic forms and FormArrays
* Form state management
* Accessibility best practices

***

## Forms Comparison

```
┌─────────────────────────────────────────────────────────────────────────┐
│              Template-Driven vs Reactive Forms                           │
├─────────────────────────────┬───────────────────────────────────────────┤
│      Template-Driven        │            Reactive Forms                  │
├─────────────────────────────┼───────────────────────────────────────────┤
│ • Logic in template         │ • Logic in component class                │
│ • Uses ngModel directive    │ • Uses FormControl, FormGroup             │
│ • Two-way data binding      │ • Explicit data flow                      │
│ • Less boilerplate          │ • More testable                           │
│ • Good for simple forms     │ • Good for complex forms                  │
│ • Async validation tricky   │ • Easy async validation                   │
│                             │ • Dynamic form generation                 │
│                             │ • Better type safety                      │
└─────────────────────────────┴───────────────────────────────────────────┘
```

***

## Template-Driven Forms

### Basic Setup

```typescript theme={null}
// Import FormsModule for template-driven forms
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-contact-form',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #contactForm="ngForm" (ngSubmit)="onSubmit(contactForm)">
      <div class="form-group">
        <label for="name">Name</label>
        <input 
          type="text"
          id="name"
          name="name"
          [(ngModel)]="model.name"
          required
          minlength="2"
          #name="ngModel"
        />
        @if (name.invalid && name.touched) {
          <div class="errors">
            @if (name.errors?.['required']) {
              <span>Name is required</span>
            }
            @if (name.errors?.['minlength']) {
              <span>Name must be at least 2 characters</span>
            }
          </div>
        }
      </div>
      
      <div class="form-group">
        <label for="email">Email</label>
        <input 
          type="email"
          id="email"
          name="email"
          [(ngModel)]="model.email"
          required
          email
          #email="ngModel"
        />
        @if (email.invalid && email.touched) {
          <div class="errors">
            @if (email.errors?.['required']) {
              <span>Email is required</span>
            }
            @if (email.errors?.['email']) {
              <span>Invalid email format</span>
            }
          </div>
        }
      </div>
      
      <div class="form-group">
        <label for="message">Message</label>
        <textarea
          id="message"
          name="message"
          [(ngModel)]="model.message"
          required
          rows="4"
        ></textarea>
      </div>
      
      <button type="submit" [disabled]="contactForm.invalid">
        Send Message
      </button>
      
      <pre>Form valid: {{ contactForm.valid }}</pre>
      <pre>Model: {{ model | json }}</pre>
    </form>
  `
})
export class ContactFormComponent {
  model = {
    name: '',
    email: '',
    message: ''
  };
  
  onSubmit(form: NgForm) {
    if (form.valid) {
      console.log('Form submitted:', this.model);
      form.reset();
    }
  }
}
```

***

## Reactive Forms

### Setup and Basic Usage

```typescript theme={null}
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-signup-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
      <div class="form-group">
        <label for="username">Username</label>
        <input 
          id="username"
          type="text"
          formControlName="username"
        />
        @if (signupForm.get('username')?.invalid && signupForm.get('username')?.touched) {
          <div class="errors">
            @if (signupForm.get('username')?.errors?.['required']) {
              <span>Username is required</span>
            }
            @if (signupForm.get('username')?.errors?.['minlength']) {
              <span>Username must be at least 3 characters</span>
            }
          </div>
        }
      </div>
      
      <div class="form-group">
        <label for="email">Email</label>
        <input 
          id="email"
          type="email"
          formControlName="email"
        />
      </div>
      
      <div class="form-group">
        <label for="password">Password</label>
        <input 
          id="password"
          type="password"
          formControlName="password"
        />
      </div>
      
      <div class="form-group">
        <label for="confirmPassword">Confirm Password</label>
        <input 
          id="confirmPassword"
          type="password"
          formControlName="confirmPassword"
        />
        @if (signupForm.errors?.['passwordMismatch']) {
          <span class="error">Passwords must match</span>
        }
      </div>
      
      <button type="submit" [disabled]="signupForm.invalid">
        Sign Up
      </button>
    </form>
  `
})
export class SignupFormComponent {
  private fb = inject(FormBuilder);
  
  signupForm = this.fb.group({
    username: ['', [Validators.required, Validators.minLength(3)]],
    email: ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, Validators.minLength(8)]],
    confirmPassword: ['', Validators.required]
  }, {
    validators: this.passwordMatchValidator
  });
  
  passwordMatchValidator(group: FormGroup) {
    const password = group.get('password')?.value;
    const confirmPassword = group.get('confirmPassword')?.value;
    return password === confirmPassword ? null : { passwordMismatch: true };
  }
  
  onSubmit() {
    if (this.signupForm.valid) {
      console.log('Form data:', this.signupForm.value);
    }
  }
}
```

### FormControl, FormGroup, FormArray

```typescript theme={null}
import { FormControl, FormGroup, FormArray, Validators } from '@angular/forms';

// FormControl - single input
const nameControl = new FormControl('', [Validators.required]);
console.log(nameControl.value);    // ''
console.log(nameControl.valid);    // false
console.log(nameControl.errors);   // { required: true }

// FormGroup - group of controls
const userForm = new FormGroup({
  name: new FormControl('', Validators.required),
  email: new FormControl('', [Validators.required, Validators.email]),
  address: new FormGroup({  // Nested group
    street: new FormControl(''),
    city: new FormControl(''),
    zip: new FormControl('')
  })
});

// Access nested controls
userForm.get('address.city')?.setValue('New York');

// FormArray - dynamic array of controls
const skills = new FormArray([
  new FormControl('TypeScript'),
  new FormControl('Angular')
]);

// Add/remove items dynamically
skills.push(new FormControl('RxJS'));
skills.removeAt(0);
```

***

## FormBuilder

The `FormBuilder` provides a cleaner API:

```typescript theme={null}
@Component({...})
export class ProfileFormComponent {
  private fb = inject(FormBuilder);
  
  profileForm = this.fb.group({
    // Shorthand: ['initialValue', validators]
    firstName: ['', Validators.required],
    lastName: ['', Validators.required],
    
    // With async validators
    email: ['', [Validators.required, Validators.email], [this.emailExistsValidator]],
    
    // Nested FormGroup
    address: this.fb.group({
      street: [''],
      city: [''],
      state: [''],
      zip: ['', Validators.pattern(/^\d{5}$/)]
    }),
    
    // FormArray
    phones: this.fb.array([
      this.fb.control('')
    ]),
    
    // With options
    newsletter: this.fb.control(false, { updateOn: 'blur' })
  });
  
  // FormArray helpers
  get phones() {
    return this.profileForm.get('phones') as FormArray;
  }
  
  addPhone() {
    this.phones.push(this.fb.control(''));
  }
  
  removePhone(index: number) {
    this.phones.removeAt(index);
  }
}
```

***

## Strongly Typed Forms

Angular 14+ provides strongly typed reactive forms. Before this feature, `form.value` returned `any`, which meant typos in field names would not be caught until runtime. With typed forms, the compiler catches mistakes at build time -- if you rename a field in your interface but forget to update the form, TypeScript tells you immediately.

```typescript theme={null}
interface UserFormModel {
  name: string;
  email: string;
  age: number;
  preferences: {
    newsletter: boolean;
    theme: 'light' | 'dark';
  };
  tags: string[];
}

@Component({...})
export class TypedFormComponent {
  private fb = inject(NonNullableFormBuilder);
  
  // Fully typed form
  userForm = this.fb.group({
    name: ['', Validators.required],
    email: ['', [Validators.required, Validators.email]],
    age: [18, [Validators.min(0), Validators.max(120)]],
    preferences: this.fb.group({
      newsletter: [false],
      theme: ['light' as const]
    }),
    tags: this.fb.array<string>([])
  });
  
  // Type-safe access
  updateTheme(theme: 'light' | 'dark') {
    this.userForm.controls.preferences.controls.theme.setValue(theme);
  }
  
  // getRawValue() returns properly typed object
  onSubmit() {
    const data = this.userForm.getRawValue();
    // data is typed as UserFormModel
    console.log(data.name, data.preferences.theme);
  }
}
```

***

## Custom Validators

### Synchronous Validators

```typescript theme={null}
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

// Factory function for configurable validator
export function minAge(min: number): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const age = control.value;
    if (age < min) {
      return { minAge: { required: min, actual: age } };
    }
    return null;
  };
}

// Simple validator
export function noWhitespace(control: AbstractControl): ValidationErrors | null {
  const hasWhitespace = /\s/.test(control.value);
  return hasWhitespace ? { noWhitespace: true } : null;
}

// Cross-field validator (for FormGroup)
export function dateRangeValidator(
  startField: string, 
  endField: string
): ValidatorFn {
  return (group: AbstractControl): ValidationErrors | null => {
    const start = group.get(startField)?.value;
    const end = group.get(endField)?.value;
    
    if (start && end && new Date(start) > new Date(end)) {
      return { dateRange: { message: 'Start date must be before end date' } };
    }
    return null;
  };
}

// Usage
const form = this.fb.group({
  age: [25, [Validators.required, minAge(18)]],
  username: ['', [Validators.required, noWhitespace]],
  startDate: [''],
  endDate: ['']
}, {
  validators: dateRangeValidator('startDate', 'endDate')
});
```

### Asynchronous Validators

```typescript theme={null}
import { AsyncValidatorFn } from '@angular/forms';
import { Observable, of, timer } from 'rxjs';
import { map, catchError, switchMap } from 'rxjs/operators';

// Async validator using inject()
export function usernameExistsValidator(): AsyncValidatorFn {
  const userService = inject(UserService);
  
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    if (!control.value) {
      return of(null);
    }
    
    // Debounce to avoid too many API calls
    return timer(300).pipe(
      switchMap(() => userService.checkUsername(control.value)),
      map(exists => exists ? { usernameTaken: true } : null),
      catchError(() => of(null))
    );
  };
}

// Usage
const form = this.fb.group({
  username: [
    '',
    [Validators.required],
    [usernameExistsValidator()]  // Async validators as third argument
  ]
});
```

***

## Dynamic Forms with FormArray

```typescript theme={null}
@Component({
  selector: 'app-order-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="orderForm" (ngSubmit)="onSubmit()">
      <h2>Order Items</h2>
      
      <div formArrayName="items">
        @for (item of items.controls; track i; let i = $index) {
          <div class="item-row" [formGroupName]="i">
            <input formControlName="name" placeholder="Item name" />
            <input formControlName="quantity" type="number" min="1" />
            <input formControlName="price" type="number" min="0" step="0.01" />
            
            <span class="subtotal">
              ${{ getSubtotal(i) | number:'1.2-2' }}
            </span>
            
            <button type="button" (click)="removeItem(i)">Remove</button>
          </div>
        }
      </div>
      
      <button type="button" (click)="addItem()">Add Item</button>
      
      <div class="summary">
        <strong>Total: ${{ total | number:'1.2-2' }}</strong>
      </div>
      
      <button type="submit" [disabled]="orderForm.invalid">
        Place Order
      </button>
    </form>
  `
})
export class OrderFormComponent {
  private fb = inject(FormBuilder);
  
  orderForm = this.fb.group({
    customerName: ['', Validators.required],
    items: this.fb.array([this.createItemGroup()])
  });
  
  get items(): FormArray {
    return this.orderForm.get('items') as FormArray;
  }
  
  get total(): number {
    return this.items.controls.reduce((sum, control, i) => {
      return sum + this.getSubtotal(i);
    }, 0);
  }
  
  createItemGroup(): FormGroup {
    return this.fb.group({
      name: ['', Validators.required],
      quantity: [1, [Validators.required, Validators.min(1)]],
      price: [0, [Validators.required, Validators.min(0)]]
    });
  }
  
  addItem() {
    this.items.push(this.createItemGroup());
  }
  
  removeItem(index: number) {
    this.items.removeAt(index);
  }
  
  getSubtotal(index: number): number {
    const item = this.items.at(index);
    return (item.get('quantity')?.value || 0) * (item.get('price')?.value || 0);
  }
  
  onSubmit() {
    if (this.orderForm.valid) {
      console.log('Order:', this.orderForm.value);
    }
  }
}
```

***

## Form State and CSS Classes

Angular automatically applies CSS classes based on form state:

```
┌─────────────────────────────────────────────────────────────────────────┐
│                    Form Control States & CSS Classes                     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   State         │ True Class      │ False Class                         │
│   ──────────────┼─────────────────┼─────────────────                    │
│   visited       │ .ng-touched     │ .ng-untouched                       │
│   changed       │ .ng-dirty       │ .ng-pristine                        │
│   valid         │ .ng-valid       │ .ng-invalid                         │
│   pending       │ .ng-pending     │ (none)                              │
│                                                                          │
│   Example CSS:                                                           │
│   ─────────────                                                          │
│   input.ng-invalid.ng-touched {                                         │
│     border-color: red;                                                   │
│   }                                                                      │
│                                                                          │
│   input.ng-valid.ng-touched {                                           │
│     border-color: green;                                                 │
│   }                                                                      │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
```

```css theme={null}
/* forms.scss */
.form-control {
  &.ng-invalid.ng-touched {
    border-color: #dc3545;
    
    &:focus {
      box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
    }
  }
  
  &.ng-valid.ng-touched {
    border-color: #28a745;
  }
  
  &.ng-pending {
    border-color: #ffc107;
  }
}

.error-message {
  color: #dc3545;
  font-size: 0.875rem;
  margin-top: 0.25rem;
}
```

***

## Error Handling Component

Create a reusable error display component:

```typescript theme={null}
// form-errors.component.ts
@Component({
  selector: 'app-form-errors',
  standalone: true,
  template: `
    @if (control && control.invalid && (control.dirty || control.touched)) {
      <div class="error-messages" role="alert">
        @for (error of errorMessages; track error) {
          <span class="error">{{ error }}</span>
        }
      </div>
    }
  `,
  styles: [`
    .error-messages {
      color: #dc3545;
      font-size: 0.875rem;
    }
    .error { display: block; }
  `]
})
export class FormErrorsComponent {
  control = input<AbstractControl | null>(null);
  
  private errorMessages$ = {
    required: 'This field is required',
    email: 'Please enter a valid email',
    minlength: (error: any) => `Minimum ${error.requiredLength} characters required`,
    maxlength: (error: any) => `Maximum ${error.requiredLength} characters allowed`,
    min: (error: any) => `Minimum value is ${error.min}`,
    max: (error: any) => `Maximum value is ${error.max}`,
    pattern: 'Invalid format'
  };
  
  get errorMessages(): string[] {
    const control = this.control();
    if (!control || !control.errors) return [];
    
    return Object.keys(control.errors).map(key => {
      const errorDef = this.errorMessages$[key];
      if (typeof errorDef === 'function') {
        return errorDef(control.errors![key]);
      }
      return errorDef || `Validation error: ${key}`;
    });
  }
}

// Usage
@Component({
  template: `
    <input formControlName="email" />
    <app-form-errors [control]="form.get('email')" />
  `,
  imports: [FormErrorsComponent, ReactiveFormsModule]
})
```

***

## Form Accessibility

```typescript theme={null}
@Component({
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <div class="form-field">
        <label for="email">
          Email
          <span class="required" aria-hidden="true">*</span>
        </label>
        <input
          id="email"
          type="email"
          formControlName="email"
          [attr.aria-invalid]="form.get('email')?.invalid"
          [attr.aria-describedby]="form.get('email')?.errors ? 'email-error' : null"
        />
        @if (form.get('email')?.invalid && form.get('email')?.touched) {
          <div id="email-error" class="error" role="alert" aria-live="polite">
            Please enter a valid email address
          </div>
        }
      </div>
      
      <div class="form-field">
        <fieldset>
          <legend>Notification Preferences</legend>
          <label>
            <input type="checkbox" formControlName="emailNotifs" />
            Email notifications
          </label>
          <label>
            <input type="checkbox" formControlName="smsNotifs" />
            SMS notifications
          </label>
        </fieldset>
      </div>
      
      <button 
        type="submit" 
        [disabled]="form.invalid"
        [attr.aria-disabled]="form.invalid"
      >
        Submit
      </button>
    </form>
  `
})
export class AccessibleFormComponent {
  form = this.fb.group({
    email: ['', [Validators.required, Validators.email]],
    emailNotifs: [true],
    smsNotifs: [false]
  });
}
```

***

## Practice Exercise

<Card title="Exercise: Build a Multi-Step Registration Form" icon="dumbbell">
  Create a registration wizard with:

  1. Step 1: Personal info (name, email)
  2. Step 2: Account details (username, password)
  3. Step 3: Preferences (newsletter, theme)
  4. Navigation between steps
  5. Validation per step
  6. Summary and submit on final step
</Card>

<Accordion title="Solution">
  ```typescript theme={null}
  interface RegistrationForm {
    personal: {
      firstName: string;
      lastName: string;
      email: string;
    };
    account: {
      username: string;
      password: string;
      confirmPassword: string;
    };
    preferences: {
      newsletter: boolean;
      theme: 'light' | 'dark';
      notifications: string[];
    };
  }

  @Component({
    selector: 'app-registration-wizard',
    standalone: true,
    imports: [ReactiveFormsModule],
    template: `
      <div class="wizard">
        <div class="steps">
          <span [class.active]="currentStep() === 1">1. Personal</span>
          <span [class.active]="currentStep() === 2">2. Account</span>
          <span [class.active]="currentStep() === 3">3. Preferences</span>
        </div>
        
        <form [formGroup]="form" (ngSubmit)="onSubmit()">
          <!-- Step 1: Personal Info -->
          @if (currentStep() === 1) {
            <div formGroupName="personal">
              <h2>Personal Information</h2>
              
              <div class="field">
                <label>First Name</label>
                <input formControlName="firstName" />
              </div>
              
              <div class="field">
                <label>Last Name</label>
                <input formControlName="lastName" />
              </div>
              
              <div class="field">
                <label>Email</label>
                <input type="email" formControlName="email" />
              </div>
            </div>
          }
          
          <!-- Step 2: Account Details -->
          @if (currentStep() === 2) {
            <div formGroupName="account">
              <h2>Account Details</h2>
              
              <div class="field">
                <label>Username</label>
                <input formControlName="username" />
              </div>
              
              <div class="field">
                <label>Password</label>
                <input type="password" formControlName="password" />
              </div>
              
              <div class="field">
                <label>Confirm Password</label>
                <input type="password" formControlName="confirmPassword" />
              </div>
              
              @if (form.get('account')?.errors?.['passwordMismatch']) {
                <span class="error">Passwords don't match</span>
              }
            </div>
          }
          
          <!-- Step 3: Preferences -->
          @if (currentStep() === 3) {
            <div formGroupName="preferences">
              <h2>Preferences</h2>
              
              <label>
                <input type="checkbox" formControlName="newsletter" />
                Subscribe to newsletter
              </label>
              
              <fieldset>
                <legend>Theme</legend>
                <label>
                  <input type="radio" formControlName="theme" value="light" />
                  Light
                </label>
                <label>
                  <input type="radio" formControlName="theme" value="dark" />
                  Dark
                </label>
              </fieldset>
              
              <h3>Summary</h3>
              <pre>{{ form.value | json }}</pre>
            </div>
          }
          
          <div class="navigation">
            @if (currentStep() > 1) {
              <button type="button" (click)="prevStep()">Previous</button>
            }
            
            @if (currentStep() < 3) {
              <button 
                type="button" 
                (click)="nextStep()"
                [disabled]="!isCurrentStepValid()"
              >
                Next
              </button>
            } @else {
              <button type="submit" [disabled]="form.invalid">
                Complete Registration
              </button>
            }
          </div>
        </form>
      </div>
    `
  })
  export class RegistrationWizardComponent {
    private fb = inject(FormBuilder);
    
    currentStep = signal(1);
    
    form = this.fb.group({
      personal: this.fb.group({
        firstName: ['', Validators.required],
        lastName: ['', Validators.required],
        email: ['', [Validators.required, Validators.email]]
      }),
      account: this.fb.group({
        username: ['', [Validators.required, Validators.minLength(3)]],
        password: ['', [Validators.required, Validators.minLength(8)]],
        confirmPassword: ['', Validators.required]
      }, { validators: this.passwordMatchValidator }),
      preferences: this.fb.group({
        newsletter: [true],
        theme: ['light' as const],
        notifications: this.fb.array([])
      })
    });
    
    passwordMatchValidator(group: AbstractControl) {
      const password = group.get('password')?.value;
      const confirm = group.get('confirmPassword')?.value;
      return password === confirm ? null : { passwordMismatch: true };
    }
    
    isCurrentStepValid(): boolean {
      const stepGroups = ['personal', 'account', 'preferences'];
      const currentGroup = this.form.get(stepGroups[this.currentStep() - 1]);
      return currentGroup?.valid ?? false;
    }
    
    nextStep() {
      if (this.isCurrentStepValid() && this.currentStep() < 3) {
        this.currentStep.update(s => s + 1);
      }
    }
    
    prevStep() {
      if (this.currentStep() > 1) {
        this.currentStep.update(s => s - 1);
      }
    }
    
    onSubmit() {
      if (this.form.valid) {
        console.log('Registration data:', this.form.value);
      }
    }
  }
  ```
</Accordion>

***

## Summary

<Steps>
  <Step title="Template-Driven Forms">
    Use FormsModule with ngModel for simple forms with two-way binding
  </Step>

  <Step title="Reactive Forms">
    Use FormControl, FormGroup, FormArray for complex, testable forms
  </Step>

  <Step title="Validators">
    Built-in validators plus custom sync/async validators
  </Step>

  <Step title="FormBuilder">
    Cleaner API for building forms with NonNullableFormBuilder for type safety
  </Step>

  <Step title="Dynamic Forms">
    Use FormArray for repeating fields and dynamic form generation
  </Step>
</Steps>

***

## Interview Deep-Dive

<AccordionGroup>
  <Accordion title="Q: You are building a multi-step checkout form with address validation, payment processing, and dynamic fields based on the selected country. Walk me through your form architecture decisions.">
    **Strong Answer:** I would use reactive forms with FormBuilder for this, not template-driven. The reasons: multi-step wizards need programmatic control over which step is valid, dynamic fields need runtime FormGroup manipulation, and payment processing needs async validators that are easier with reactive forms.

    My architecture: one top-level FormGroup with nested FormGroups for each step -- personal, shipping, payment. Each step component receives its FormGroup as an input and renders the fields. The parent wizard component owns the entire form and controls navigation. This separation means step components are reusable and testable in isolation.

    For dynamic country-based fields: when the user selects a country, I dynamically add or remove FormControls. US addresses need state and zip. UK addresses need postcode and county. I listen to the country control's valueChanges observable and call addControl/removeControl on the address FormGroup. I also swap validators -- US zip needs a 5-digit pattern, UK postcode needs a different pattern.

    For async validation on the address, I use an async validator that calls an address verification API with debounce (to avoid calling on every keystroke). The form shows a "validating..." state (ng-pending class) while the API call is in flight.

    For step navigation: each step checks its own FormGroup validity before allowing "Next." The final step can validate the entire form at once. I persist form state to localStorage via an effect, so the user can resume if they accidentally close the tab.

    **Follow-up:** How do you handle the password field validation where confirm-password must match password?
    **Answer:** I use a cross-field validator at the FormGroup level, not on individual controls. The validator function receives the group, reads both password and confirmPassword values, and returns an error object if they do not match. This is important because the error lives on the group, not on either control -- so the template needs to check formGroup.errors?.passwordMismatch, not control.errors. A common mistake is putting the validator on confirmPassword, which does not have access to the password value.
  </Accordion>

  <Accordion title="Q: Explain the difference between NonNullableFormBuilder and the regular FormBuilder. Why does it matter for type safety?">
    **Strong Answer:** Regular FormBuilder creates controls where the value type includes null and undefined. When you call form.reset(), all controls are set to null (not their initial values). So form.controls.name.value has type string | null, and you need null checks everywhere.

    NonNullableFormBuilder changes two things. First, reset() restores controls to their initial values instead of null. Second, the TypeScript types reflect this -- form.controls.name.value is string, not string | null. This eliminates an entire category of null-check boilerplate.

    In practice, this matters for type safety downstream. If you pass form.getRawValue() to an API service that expects a User object (no null fields), the regular FormBuilder version will not type-check without explicit casting or null assertions. The NonNullableFormBuilder version matches the API type naturally.

    The gotcha: NonNullableFormBuilder does not prevent individual controls from being explicitly set to null via setValue(null). It only affects the reset behavior and the inferred types. If your form legitimately needs nullable fields (like an optional date field), you still need to declare those as FormControl\<string | null>.

    **Follow-up:** How do strongly typed forms interact with dynamic FormArrays?
    **Answer:** FormArrays with typed forms require you to specify the type of controls they contain. this.fb.array\<FormGroup\<ItemForm>>(\[]) tells TypeScript that each element is a FormGroup matching the ItemForm interface. When you push or access elements, TypeScript knows the shape. The challenge is that dynamically created controls need to match the declared type exactly. If your ItemForm has required fields, you must initialize them when creating new form groups.
  </Accordion>

  <Accordion title="Q: How would you implement a custom form control (ControlValueAccessor) and why would you need one?">
    **Strong Answer:** ControlValueAccessor is the bridge between Angular's forms API and custom UI components. Once implemented, your component works with formControlName, ngModel, validation, disabled state -- all the form features. The classic use cases: star rating widgets, rich text editors, date range pickers, tag inputs, or any UI element that does not map to a native HTML input.

    You implement four methods: writeValue (called when the form sets the control's value), registerOnChange (gives you a callback to call when the user changes the value), registerOnTouched (callback for when the control is blurred), and optionally setDisabledState. You also register the component as a NG\_VALUE\_ACCESSOR provider using forwardRef.

    The key insight that separates a good implementation from a fragile one: writeValue can be called at any time, including before ngOnInit. So do not assume the component is fully initialized when writeValue runs. Also, call the onChange callback only on user interaction, not in writeValue -- otherwise you create infinite loops where the form sets a value, which triggers onChange, which updates the form, which calls writeValue again.

    I always test CVA components by using them inside a reactive form in the test, setting values programmatically, checking that the form value updates when the user interacts, and verifying that disabled state propagates correctly.

    **Follow-up:** Can you use signals inside a ControlValueAccessor implementation?
    **Answer:** Yes, and it works well. You can store the internal value as a signal, use it in the template with signal reads, and call the registered onChange callback in the methods that respond to user interaction. The writeValue method calls this.value.set(newValue). This gives you reactive templates while maintaining full form integration. Just be careful not to trigger onChange from an effect that reads the value signal, as that creates a feedback loop.
  </Accordion>
</AccordionGroup>

***

## Next Steps

<Card title="Next: HTTP Client & RxJS" icon="arrow-right" href="/courses/angular-crash-course/08-http-rxjs">
  Connect to APIs and master reactive data streams
</Card>
