Angular
08 / 10

Forms (Template-driven & Reactive)

Angular Forms

Angular provides two approaches to handling user input through forms: template-driven forms and reactive forms. Both capture user input events, validate input, and create a form model.

Template-Driven Forms

Template-driven forms rely on directives in the template to create and manipulate the form model. They are simpler and work well for basic forms.

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-template-form',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
      <div>
        <label>Name:
          <input
            type="text"
            name="name"
            [(ngModel)]="user.name"
            required
            minlength="3"
            #name="ngModel"
          />
        </label>
        <div *ngIf="name.invalid && name.touched">
          <small *ngIf="name.errors?.['required']">Name is required</small>
          <small *ngIf="name.errors?.['minlength']">
            Name must be at least 3 characters
          </small>
        </div>
      </div>
      
      <div>
        <label>Email:
          <input
            type="email"
            name="email"
            [(ngModel)]="user.email"
            required
            email
            #email="ngModel"
          />
        </label>
        <div *ngIf="email.invalid && email.touched">
          <small *ngIf="email.errors?.['required']">Email is required</small>
          <small *ngIf="email.errors?.['email']">Invalid email format</small>
        </div>
      </div>
      
      <div>
        <label>Age:
          <input
            type="number"
            name="age"
            [(ngModel)]="user.age"
            required
            min="18"
            max="100"
          />
        </label>
      </div>
      
      <button type="submit" [disabled]="userForm.invalid">Submit</button>
    </form>
    
    <div *ngIf="submitted">
      <h3>Submitted Data:</h3>
      <pre>{{ user | json }}</pre>
    </div>
  `
})
export class TemplateFormComponent {
  user = {
    name: '',
    email: '',
    age: null
  };
  
  submitted = false;
  
  onSubmit(form: any) {
    if (form.valid) {
      console.log('Form submitted:', this.user);
      this.submitted = true;
    }
  }
}

Reactive Forms

Reactive forms provide a model-driven approach to handling form inputs. They offer more robust validation, better testability, and are more suitable for complex forms.

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-reactive-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="userForm" (ngSubmit)="onSubmit()">
      <div>
        <label>Name:
          <input type="text" formControlName="name" />
        </label>
        <div *ngIf="name?.invalid && name?.touched">
          <small *ngIf="name?.errors?.['required']">Name is required</small>
          <small *ngIf="name?.errors?.['minlength']">
            Name must be at least 3 characters
          </small>
        </div>
      </div>
      
      <div>
        <label>Email:
          <input type="email" formControlName="email" />
        </label>
        <div *ngIf="email?.invalid && email?.touched">
          <small *ngIf="email?.errors?.['required']">Email is required</small>
          <small *ngIf="email?.errors?.['email']">Invalid email</small>
        </div>
      </div>
      
      <div>
        <label>Password:
          <input type="password" formControlName="password" />
        </label>
        <div *ngIf="password?.invalid && password?.touched">
          <small *ngIf="password?.errors?.['required']">Password is required</small>
          <small *ngIf="password?.errors?.['minlength']">
            Password must be at least 8 characters
          </small>
        </div>
      </div>
      
      <div formGroupName="address">
        <h3>Address</h3>
        <label>Street:
          <input type="text" formControlName="street" />
        </label>
        <label>City:
          <input type="text" formControlName="city" />
        </label>
        <label>Zip:
          <input type="text" formControlName="zip" />
        </label>
      </div>
      
      <button type="submit" [disabled]="userForm.invalid">Submit</button>
    </form>
    
    <div>
      <p>Form Status: {{ userForm.status }}</p>
      <p>Form Value: {{ userForm.value | json }}</p>
    </div>
  `
})
export class ReactiveFormComponent implements OnInit {
  userForm!: FormGroup;
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit() {
    // Using FormBuilder (recommended)
    this.userForm = this.fb.group({
      name: ['', [Validators.required, Validators.minLength(3)]],
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(8)]],
      address: this.fb.group({
        street: [''],
        city: ['', Validators.required],
        zip: ['', [Validators.required, Validators.pattern(/^\d{5}$/)]] 
      })
    });
    
    // Or using FormControl directly
    // this.userForm = new FormGroup({
    //   name: new FormControl('', [Validators.required]),
    //   email: new FormControl('', [Validators.required, Validators.email])
    // });
  }
  
  // Getters for easy access in template
  get name() { return this.userForm.get('name'); }
  get email() { return this.userForm.get('email'); }
  get password() { return this.userForm.get('password'); }
  
  onSubmit() {
    if (this.userForm.valid) {
      console.log('Form submitted:', this.userForm.value);
      // Reset form
      this.userForm.reset();
    } else {
      // Mark all as touched to show validation errors
      this.userForm.markAllAsTouched();
    }
  }
}

Custom Validators

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

// Sync validator
export function forbiddenNameValidator(forbiddenName: RegExp): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const forbidden = forbiddenName.test(control.value);
    return forbidden ? { forbiddenName: { value: control.value } } : null;
  };
}

// Password match validator
export function passwordMatchValidator(): ValidatorFn {
  return (formGroup: AbstractControl): ValidationErrors | null => {
    const password = formGroup.get('password');
    const confirmPassword = formGroup.get('confirmPassword');
    
    if (!password || !confirmPassword) {
      return null;
    }
    
    return password.value === confirmPassword.value
      ? null
      : { passwordMismatch: true };
  };
}

// Async validator (e.g., check username availability)
export function usernameAvailableValidator(userService: UserService): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    if (!control.value) {
      return of(null);
    }
    
    return userService.checkUsername(control.value).pipe(
      map(available => available ? null : { usernameTaken: true }),
      catchError(() => of(null))
    );
  };
}

// Usage
this.userForm = this.fb.group({
  username: ['', 
    [Validators.required, forbiddenNameValidator(/admin/i)],
    [usernameAvailableValidator(this.userService)] // async
  ],
  password: ['', [Validators.required, Validators.minLength(8)]],
  confirmPassword: ['', Validators.required]
}, { 
  validators: passwordMatchValidator() // form-level validator
});

Dynamic Forms & FormArray

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms';

@Component({
  selector: 'app-dynamic-form',
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <div formArrayName="skills">
        <h3>Skills</h3>
        <div *ngFor="let skill of skills.controls; let i = index" [formGroupName]="i">
          <input formControlName="name" placeholder="Skill name" />
          <input formControlName="years" type="number" placeholder="Years" />
          <button type="button" (click)="removeSkill(i)">Remove</button>
        </div>
        <button type="button" (click)="addSkill()">Add Skill</button>
      </div>
      
      <button type="submit" [disabled]="form.invalid">Submit</button>
    </form>
    
    <pre>{{ form.value | json }}</pre>
  `
})
export class DynamicFormComponent implements OnInit {
  form!: FormGroup;
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit() {
    this.form = this.fb.group({
      name: ['', Validators.required],
      skills: this.fb.array([
        this.createSkill()
      ])
    });
  }
  
  get skills(): FormArray {
    return this.form.get('skills') as FormArray;
  }
  
  createSkill(): FormGroup {
    return this.fb.group({
      name: ['', Validators.required],
      years: [0, [Validators.required, Validators.min(0)]]
    });
  }
  
  addSkill() {
    this.skills.push(this.createSkill());
  }
  
  removeSkill(index: number) {
    this.skills.removeAt(index);
  }
  
  onSubmit() {
    if (this.form.valid) {
      console.log('Submitted:', this.form.value);
    }
  }
}

Form State & Validation

// Form control states
const control = this.form.get('email');

// Value
console.log(control?.value);

// Validity
console.log(control?.valid);      // true if valid
console.log(control?.invalid);    // true if invalid
console.log(control?.errors);     // validation errors object

// Touch state
console.log(control?.touched);    // true after blur
console.log(control?.untouched);  // opposite of touched
console.log(control?.dirty);      // true after value change
console.log(control?.pristine);   // opposite of dirty

// Programmatic updates
control?.setValue('new value');           // Set value
control?.patchValue('partial');           // Patch value
control?.markAsTouched();                 // Mark as touched
control?.markAsDirty();                   // Mark as dirty
control?.updateValueAndValidity();        // Recalculate validity

// Listen to value changes
control?.valueChanges.subscribe(value => {
  console.log('Value changed:', value);
});

// Listen to status changes
control?.statusChanges.subscribe(status => {
  console.log('Status changed:', status); // 'VALID', 'INVALID', 'PENDING'
});

// Disable/Enable
control?.disable();
control?.enable();

// Reset
this.form.reset();
this.form.reset({ email: 'default@example.com' });

// Patch form
this.form.patchValue({
  name: 'John',
  email: 'john@example.com'
});

// Set entire form value
this.form.setValue({
  name: 'John',
  email: 'john@example.com',
  address: {
    street: '123 Main St',
    city: 'NYC',
    zip: '10001'
  }
});

Template-Driven vs Reactive Forms

**Template-Driven**: Good for simple forms, less code, easier to understand. **Reactive**: Better for complex forms, easier to test, more control, dynamic validation, better for large-scale apps.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free