Docs
/
Angular
Chapter 8

08 — Template-Driven Forms

When to Use Template-Driven Forms

ScenarioUse
Simple login/search form✅ Template-driven
Complex multi-step form❌ Use Reactive
Dynamic fields (add/remove)❌ Use Reactive
Heavy custom validation❌ Use Reactive
Quick prototype✅ Template-driven

Setup

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

@Component({
  imports: [FormsModule],
})

Basic Form with ngModel

@Component({
  imports: [FormsModule],
  template: `
    <form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
      <div>
        <label for="name">Name</label>
        <input
          id="name"
          name="name"
          [(ngModel)]="user.name"
          required
          minlength="2"
          #nameField="ngModel"
        >
        @if (nameField.invalid && nameField.touched) {
          <span class="error">
            @if (nameField.errors?.['required']) { Name is required }
            @if (nameField.errors?.['minlength']) { Min 2 characters }
          </span>
        }
      </div>

      <div>
        <label for="email">Email</label>
        <input
          id="email"
          name="email"
          type="email"
          [(ngModel)]="user.email"
          required
          email
          #emailField="ngModel"
        >
        @if (emailField.invalid && emailField.touched) {
          <span class="error">Valid email required</span>
        }
      </div>

      <div>
        <label for="role">Role</label>
        <select id="role" name="role" [(ngModel)]="user.role" required>
          <option value="">Select role</option>
          @for (role of roles; track role) {
            <option [value]="role">{{ role | titlecase }}</option>
          }
        </select>
      </div>

      <div>
        <label>
          <input type="checkbox" name="agree" [(ngModel)]="user.agree" required>
          I agree to terms
        </label>
      </div>

      <button [disabled]="userForm.invalid">Submit</button>
    </form>
  `,
})
export class UserFormComponent {
  user = {
    name: '',
    email: '',
    role: '',
    agree: false,
  };

  roles = ['user', 'admin', 'editor'];

  onSubmit(form: NgForm) {
    if (form.valid) {
      console.log('Form value:', form.value);
      form.resetForm();             // Reset form + clear validation state
    }
  }
}

Built-in Validation Directives

DirectiveValidator
requiredField must have a value
emailValid email format
minlength="n"Minimum length
maxlength="n"Maximum length
pattern="regex"Regex match
min="n"Minimum number
max="n"Maximum number

CSS Classes Added by Angular

Angular automatically adds CSS classes based on control state:

StateTrue ClassFalse Class
Visited.ng-touched.ng-untouched
Changed.ng-dirty.ng-pristine
Valid.ng-valid.ng-invalid
// Show red border on touched + invalid fields
input.ng-invalid.ng-touched {
  border-color: red;
}

input.ng-valid.ng-touched {
  border-color: green;
}

Custom Validator Directive

import { Directive } from '@angular/core';
import { NG_VALIDATORS, Validator, AbstractControl, ValidationErrors } from '@angular/forms';

@Directive({
  selector: '[appNoWhitespace]',
  standalone: true,
  providers: [
    { provide: NG_VALIDATORS, useExisting: NoWhitespaceDirective, multi: true },
  ],
})
export class NoWhitespaceDirective implements Validator {
  validate(control: AbstractControl): ValidationErrors | null {
    if (!control.value) return null;
    return control.value.trim().length === 0 ? { whitespace: true } : null;
  }
}
<input name="username" [(ngModel)]="username" appNoWhitespace>

Grouped Fields with ngModelGroup

<form #f="ngForm">
  <div ngModelGroup="address" #addressGroup="ngModelGroup">
    <input name="street" [(ngModel)]="address.street" required>
    <input name="city"   [(ngModel)]="address.city" required>
    <input name="zip"    [(ngModel)]="address.zip" required pattern="^\d{5}$">
  </div>

  @if (addressGroup.invalid && addressGroup.touched) {
    <span class="error">Please complete all address fields</span>
  }
</form>

<!-- f.value → { address: { street, city, zip } } -->

Reactive vs Template-Driven — Side by Side

Template-Driven

<input name="email" [(ngModel)]="email" required email #e="ngModel">
@if (e.errors?.['required']) { <span>Required</span> }

Reactive

email = new FormControl('', [Validators.required, Validators.email]);
<input [formControl]="email">
@if (email.errors?.['required']) { <span>Required</span> }

Key Takeaways

  • Template-driven forms use FormsModule + [(ngModel)] + validation directives
  • Every form control needs a name attribute — Angular uses it as the key
  • Use #field="ngModel" to get a reference for error checking in templates
  • CSS classes (.ng-invalid.ng-touched) are free — use them for styling
  • Use ngModelGroup to group related fields (address, payment, etc.)
  • Prefer Reactive Forms for complex forms — template-driven forms don't scale well