Forms

Template-Driven Forms

Build forms with Angular's template-driven approach using ngModel, validation directives, and template reference variables.

What Are Template-Driven Forms?

Template-driven forms rely on directives in the template to create and manage form controls. The logic lives primarily in the HTML template, making them simple and quick to build.

FeatureTemplate-DrivenReactive
Logic locationTemplate (HTML)Component (TypeScript)
Data modelTwo-way binding (ngModel)FormGroup / FormControl
ValidationTemplate directivesProgrammatic validators
Best forSimple formsComplex/dynamic forms
TestingHarder to unit testEasy to unit test

Setup

Import FormsModule in your component:

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

@Component({
  selector: 'app-login-form',
  standalone: true,
  imports: [FormsModule],
  templateUrl: './login-form.component.html',
})
export class LoginFormComponent {
  // ...
}

Basic Form

typescript
@Component({
  selector: 'app-contact-form',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #contactForm="ngForm" (ngSubmit)="onSubmit(contactForm)">
      <div>
        <label for="name">Name</label>
        <input id="name"
               name="name"
               [(ngModel)]="model.name"
               required
               minlength="2"
               #name="ngModel">

        @if (name.invalid && name.touched) {
          <div class="error">
            @if (name.errors?.['required']) {
              <p>Name is required.</p>
            }
            @if (name.errors?.['minlength']) {
              <p>Name must be at least 2 characters.</p>
            }
          </div>
        }
      </div>

      <div>
        <label for="email">Email</label>
        <input id="email"
               name="email"
               type="email"
               [(ngModel)]="model.email"
               required
               email
               #emailField="ngModel">

        @if (emailField.invalid && emailField.touched) {
          <div class="error">
            @if (emailField.errors?.['required']) {
              <p>Email is required.</p>
            }
            @if (emailField.errors?.['email']) {
              <p>Please enter a valid email.</p>
            }
          </div>
        }
      </div>

      <div>
        <label for="message">Message</label>
        <textarea id="message"
                  name="message"
                  [(ngModel)]="model.message"
                  required
                  minlength="10"
                  #message="ngModel"></textarea>

        @if (message.invalid && message.touched) {
          <div class="error">Message must be at least 10 characters.</div>
        }
      </div>

      <button type="submit" [disabled]="contactForm.invalid">
        Send Message
      </button>
    </form>
  `,
})
export class ContactFormComponent {
  model = {
    name: '',
    email: '',
    message: '',
  };

  onSubmit(form: NgForm) {
    if (form.valid) {
      console.log('Form submitted:', this.model);
      // Send to API
      form.reset(); // Reset the form
    }
  }
}

Key Concepts

ngModel — Two-Way Binding

html
<!-- Two-way binding -->
<input [(ngModel)]="user.name" name="name">

<!-- One-way binding (read from model) -->
<input [ngModel]="user.name" name="name">

<!-- Event binding (write to model) -->
<input (ngModelChange)="onNameChange($event)" name="name">

Important: Every form control with ngModel must have a name attribute.

Template Reference Variables

html
<!-- #myField="ngModel" gives access to the control's state -->
<input name="email" ngModel required #emailRef="ngModel">

<p>Valid: {{ emailRef.valid }}</p>
<p>Touched: {{ emailRef.touched }}</p>
<p>Value: {{ emailRef.value }}</p>

Form Reference

html
<form #myForm="ngForm" (ngSubmit)="submit(myForm)">
  <!-- controls -->
</form>

<p>Form valid: {{ myForm.valid }}</p>
<p>Form dirty: {{ myForm.dirty }}</p>
<p>Form values: {{ myForm.value | json }}</p>

Form Control States

Angular tracks the state of each control and the form:

StateClass (true)Class (false)Meaning
Touched.ng-touched.ng-untouchedUser has focused and blurred
Dirty.ng-dirty.ng-pristineValue has been changed
Valid.ng-valid.ng-invalidAll validators pass

Styling Based on State

css
input.ng-invalid.ng-touched {
  border-color: red;
}

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

.error {
  color: red;
  font-size: 0.85em;
  margin-top: 4px;
}

Built-in Validators

DirectivePurposeExample
requiredField must have a value<input required>
minlengthMinimum character count<input minlength="3">
maxlengthMaximum character count<input maxlength="100">
patternRegex pattern match<input pattern="[a-zA-Z]+">
emailValid email format<input email>
minMinimum number value<input type="number" min="0">
maxMaximum number value<input type="number" max="100">

Different Input Types

Select Dropdown

html
<select name="country" [(ngModel)]="model.country" required>
  <option value="" disabled>Select a country</option>
  @for (country of countries; track country.code) {
    <option [value]="country.code">{{ country.name }}</option>
  }
</select>

Radio Buttons

html
<div>
  <label>
    <input type="radio" name="plan" [(ngModel)]="model.plan" value="free">
    Free
  </label>
  <label>
    <input type="radio" name="plan" [(ngModel)]="model.plan" value="pro">
    Pro
  </label>
  <label>
    <input type="radio" name="plan" [(ngModel)]="model.plan" value="enterprise">
    Enterprise
  </label>
</div>

Checkbox

html
<label>
  <input type="checkbox" name="terms" [(ngModel)]="model.agreeToTerms" required>
  I agree to the terms and conditions
</label>

Complete Registration Form

typescript
@Component({
  selector: 'app-register',
  standalone: true,
  imports: [FormsModule],
  template: `
    <h2>Create Account</h2>
    <form #regForm="ngForm" (ngSubmit)="register(regForm)">

      <div class="form-group">
        <label>Full Name</label>
        <input name="fullName" [(ngModel)]="user.fullName"
               required minlength="2" #fullName="ngModel">
        @if (fullName.invalid && fullName.touched) {
          <small class="error">Enter your full name (min 2 chars)</small>
        }
      </div>

      <div class="form-group">
        <label>Email</label>
        <input name="email" type="email" [(ngModel)]="user.email"
               required email #email="ngModel">
        @if (email.invalid && email.touched) {
          <small class="error">Valid email required</small>
        }
      </div>

      <div class="form-group">
        <label>Password</label>
        <input name="password" type="password" [(ngModel)]="user.password"
               required minlength="8" #password="ngModel">
        @if (password.invalid && password.touched) {
          <small class="error">Password must be at least 8 characters</small>
        }
      </div>

      <div class="form-group">
        <label>Role</label>
        <select name="role" [(ngModel)]="user.role" required>
          <option value="">Select role</option>
          <option value="student">Student</option>
          <option value="professional">Professional</option>
          <option value="educator">Educator</option>
        </select>
      </div>

      <label class="checkbox">
        <input type="checkbox" name="terms" [(ngModel)]="user.agreeToTerms" required>
        I agree to the Terms of Service
      </label>

      <button type="submit" [disabled]="regForm.invalid">
        Create Account
      </button>

      <p class="form-status" *ngIf="submitted">
        ✅ Account created successfully!
      </p>
    </form>
  `,
})
export class RegisterComponent {
  user = {
    fullName: '',
    email: '',
    password: '',
    role: '',
    agreeToTerms: false,
  };
  submitted = false;

  register(form: NgForm) {
    if (form.valid) {
      console.log('Registration:', this.user);
      this.submitted = true;
      form.reset();
    }
  }
}

When to Use Template-Driven Forms

✅ Simple forms (login, contact, search) ✅ Quick prototyping ✅ Forms with minimal validation ✅ When you prefer keeping logic in the template

❌ Complex dynamic forms (fields added/removed at runtime) ❌ Forms requiring advanced validation logic ❌ When you need extensive unit testing of form logic

Next Steps

For more complex requirements, Angular offers reactive forms — a powerful programmatic approach to building and validating forms entirely in TypeScript.