Skip to content

Angular Template-Driven Forms

Template-driven forms build most of the form's structure directly in the HTML template, using directives Angular provides through FormsModule. This lesson covers building and validating one from start to finish.

How Template-Driven Forms Work

Template-driven forms rely on the ngModel directive to bind each input to a component property, and Angular automatically builds an internal form model (an NgForm) tracking the whole form's state behind the scenes.

This approach requires very little TypeScript, most of the form's shape and validation rules are expressed directly as HTML attributes and directives.

@Component({
  selector: 'app-contact-form',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #contactForm="ngForm" (ngSubmit)="onSubmit(contactForm)">
      <input name="email" [(ngModel)]="email" required email />
      <button type="submit" [disabled]="contactForm.invalid">Send</button>
    </form>
  `,
})
export class ContactFormComponent {
  email = '';
  onSubmit(form: NgForm) {
    console.log(form.value);
  }
}

#contactForm="ngForm" exposes the whole form's state as a template reference variable you can read (.invalid) or pass to a method.

Template-Driven Forms Syntax

<form #f="ngForm" (ngSubmit)="onSubmit(f)">
  <input name="field" [(ngModel)]="value" required minlength="3" />
</form>
  • Every ngModel-bound field needs a name attribute, Angular uses it internally to track the control.
  • #f="ngForm" exposes the NgForm directive instance as a template variable with .value, .valid, and .controls.
  • Native HTML validation attributes (required, minlength, pattern, email) double as Angular validators.
  • (ngSubmit) fires on submission and correctly prevents the default browser form submission.

Template-Driven Forms Cheatsheet

Directives and attributes for building forms declaratively in the template.

Directive/Attribute Example Purpose
ngModel [(ngModel)]="email" Two-way bind an input to a class property
name name="email" Required alongside ngModel to register the control
ngForm #f="ngForm" Access the whole form's state
required required Field must be non-empty
minlength / maxlength minlength="3" Length constraints
pattern pattern="[0-9]+" Regex validation
email email Validates email format
ngModelGroup ngModelGroup="address" Group related fields together

Reading Form and Field State

Both the whole form (via #f="ngForm") and individual fields (via a per-field template variable like #email="ngModel") expose validity and interaction state you can use to show contextual error messages.

<input name="email" [(ngModel)]="email" required email #emailField="ngModel" />

<p *ngIf="emailField.invalid && emailField.touched">
  Please enter a valid email address.
</p>

Grouping Fields With ngModelGroup

Related fields (like an address's street, city, and zip) can be grouped together with ngModelGroup, producing a nested object in the overall form value instead of flat, top-level properties.

<div ngModelGroup="address">
  <input name="street" [(ngModel)]="address.street" />
  <input name="city" [(ngModel)]="address.city" />
</div>

<!-- form.value becomes: { address: { street: '...', city: '...' } } -->

A Complete Template-Driven Form

<form #signupForm="ngForm" (ngSubmit)="onSubmit(signupForm)">
  <input name="username" [(ngModel)]="model.username" required minlength="3" />
  <input name="email" [(ngModel)]="model.email" required email />
  <button type="submit" [disabled]="signupForm.invalid">Sign Up</button>
</form>

Common Mistakes

  • Forgetting the name attribute on an ngModel-bound input, Angular cannot register the control without it.
  • Showing validation errors immediately on page load instead of gating them behind .touched or .dirty.
  • Trying to unit test form logic that only exists inside the template, which requires rendering the component.
  • Using template-driven forms for a large, dynamic form with many conditional fields, which quickly becomes unwieldy.

Key Takeaways

  • Template-driven forms use ngModel and FormsModule to build form structure mostly in the template.
  • #form="ngForm" exposes the whole form's state; per-field variables expose individual control state.
  • Native HTML validation attributes double as Angular validators in template-driven forms.
  • ngModelGroup groups related fields into a nested value object.

Pro Tip

Gate validation error messages behind field.touched || field.dirty (or check submitted after form submission), showing every error the instant the page loads creates a poor, alarming first impression for users.