Template-Driven Forms

Chapter 8
Template-Driven Forms
Building forms whose logic lives mostly in the template, with validation handled by directives

Angular has two distinct, official approaches to forms — template-driven (this chapter) and reactive (next chapter). Template-driven forms keep most of the form's setup in the HTML template itself, built on the ngModel two-way binding from Chapter 3. They're the quicker option for simpler forms, and the closer fit to how React's controlled inputs felt.

The Setup — ngForm and ngModel Together

// signup.component.ts import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; @Component({ selector: 'app-signup', standalone: true, imports: [FormsModule], templateUrl: './signup.component.html', }) export class SignupComponent { model = { name: '', email: '' }; onSubmit() { console.log(this.model); } }
<!-- signup.component.html --> <form #signupForm="ngForm" (ngSubmit)="onSubmit()"> <input name="name" [(ngModel)]="model.name" required /> <input name="email" [(ngModel)]="model.email" required email /> <button type="submit" [disabled]="signupForm.invalid">Sign up</button> </form>

Three pieces work together here. #signupForm="ngForm" is a template reference variable — it captures the form's auto-created NgForm object, giving access to its overall validity (signupForm.invalid). (ngSubmit) fires the handler on submit, already preventing the native page reload that React needed preventDefault() for (Fundamentals Chapter 4). Each [(ngModel)] input needs a name attribute so the form can track it as a named control.

Validation Through Directives

Validation is declared as plain attributes on the inputs — required, email, minlength, maxlength, pattern. Angular wires these standard-looking HTML attributes into its own validation system, tracking each control's validity and the overall form's. [disabled]="signupForm.invalid" then disables the submit button until every validator passes — declarative validation with no JavaScript validation code written at all.

Showing Validation Messages

<input name="email" #email="ngModel" [(ngModel)]="model.email" required email /> @if (email.invalid && email.touched) { <small>A valid email is required.</small> }

A per-input reference variable — #email="ngModel" — captures that single control's state, exposing flags like invalid, valid, touched (the user has focused then left it), and dirty (the value has changed). Combining invalid && touched with the @if from Chapter 4 shows an error message only after the user has actually interacted with the field — not immediately on an untouched, empty form.

touched and dirty mirror good form UX
These control-state flags exist precisely to support the standard "don't yell at the user before they've done anything" pattern. touched waits until they've left a field; dirty waits until they've changed it. React projects had to track this kind of "has the user interacted yet" state manually — Angular's forms provide it built in, on every control.
Every ngModel input needs a unique name attribute
Inside a form, a [(ngModel)] control without a name attribute (or with a duplicate one) throws a runtime error — the form uses name as the key to register and track each control. This is easy to forget when copying the simpler standalone ngModel usage from Chapter 3, which worked without a name because it wasn't inside an ngForm.
Angular template-drivenReact controlled-form equivalent
[(ngModel)]="model.x"value + onChange
(ngSubmit)="onSubmit()"onSubmit + preventDefault()
required / email / minlengthHand-written validation logic
control.touched / .dirtyManually tracked "interacted" state

Coding Challenges

Challenge 1

Build a template-driven form with name and email fields (both required, email also using the email validator), a model object, and a submit handler that logs the model — with the submit button disabled while the form is invalid.

📄 View solution
Challenge 2

Add per-field validation messages to the form, each shown only when that field is both invalid and touched, using a per-input #ref="ngModel" reference variable and @if.

📄 View solution
Challenge 3

Build a template-driven "create account" form with a username (required, minlength 3) and a password (required, minlength 8), showing the specific reason a field is invalid (e.g. "too short" vs "required") by checking the control's errors.

📄 View solution

Chapter 8 Quick Reference

  • Template-driven forms build on [(ngModel)] and require FormsModule
  • #form="ngForm" — a template ref capturing the whole form's state (e.g. form.invalid)
  • (ngSubmit) — submit handler; already prevents the native page reload
  • Validation is declared as attributes: required, email, minlength, pattern
  • #field="ngModel" — a per-control ref exposing invalid/touched/dirty/errors
  • Every ngModel control inside a form needs a unique name attribute
  • Next chapter: reactive forms — the same goals, but defined in the component class instead