Angular Intermediate #2: Template-driven Forms and a Comparison

7 min read

In #1 we covered one of Angular’s two form approaches — Reactive Forms. You build FormGroup and FormControl directly in the component class, and the template just connects to those objects. But Angular has another form approach. That’s Template-driven Forms.

As the name suggests, in this approach the entire form’s logic lives on the template (HTML). You don’t declare a separate form object in the component class; instead, a directive called ngModel does almost all the work. In this post, we walk through how to use Template-driven Forms, then put the two approaches side by side at the end.

What is Template-driven? #

Template-driven Forms are exactly that — the template drives the form. The structure, the validation rules, the state tracking — Angular infers all of it from directives written inside the HTML.

The key is a directive called ngModel. We actually met it briefly in Basics #3 when we covered two-way binding. Back then we used it as a simple way to bind a variable to an input — [(ngModel)]="name" — but ngModel was doing much more than that. When used inside a form, it automatically registers that input as a control on the form and tracks its value and validation state (valid/invalid, touched/dirty, etc.) the whole time.

Setting up FormsModule #

To use Template-driven Forms, import FormsModule. In a standalone component, add it directly to the component’s imports array.

src/app/signup-form.component.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-signup-form',
  standalone: true,
  imports: [FormsModule],
  templateUrl: './signup-form.component.html',
})
export class SignupFormComponent {
  // Directives like ngModel and ngForm are now usable in the template
}

For Reactive Forms we imported ReactiveFormsModule. For Template-driven, you swap that out for FormsModule. Importing both at the same time doesn’t conflict, but in a single component it’s cleaner to commit to one approach.

Two-way binding with [(ngModel)] #

Let’s start with the simplest example.

src/app/signup-form.component.html
<input
  type="text"
  name="username"
  [(ngModel)]="username"
  placeholder="Username"
/>
<p>You typed: {{ username }}</p>
src/app/signup-form.component.ts
export class SignupFormComponent {
  username = '';
}

There’s one important rule here. When using ngModel inside a form, you must add a name attribute. Angular uses that name value as the key to register the control inside the form object. Without name, you’ll see a console error and the form won’t work properly.

Note
When you use [(ngModel)] purely to bind an input value to a variable outside a form, the name attribute isn’t required. But the moment it’s inside a <form> tag, name becomes mandatory. Think of it as a rule: inside a form, name is always required.

ngForm and (ngSubmit) #

Now let’s handle the whole form. When you use a <form> tag, Angular automatically attaches an NgForm instance there. To access that object, grab a reference to it with template variable syntax (#).

src/app/signup-form.component.html
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
  <label>
    Username
    <input type="text" name="username" [(ngModel)]="model.username" />
  </label>

  <label>
    Email
    <input type="email" name="email" [(ngModel)]="model.email" />
  </label>

  <button type="submit">Sign up</button>
</form>

<p>Form valid? {{ userForm.valid }}</p>
src/app/signup-form.component.ts
import { Component } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';

@Component({
  selector: 'app-signup-form',
  standalone: true,
  imports: [FormsModule],
  templateUrl: './signup-form.component.html',
})
export class SignupFormComponent {
  model = { username: '', email: '' };

  onSubmit(form: NgForm) {
    if (form.valid) {
      console.log('submitted:', form.value);
    }
  }
}

#userForm="ngForm" says: “grab the ngForm directive instance attached to this form, and call it userForm.” Once you have that handle, you can work with the entire form object both inside the template and inside the component class via (ngSubmit).

The reason for (ngSubmit) instead of (submit) is that (ngSubmit) automatically prevents the browser’s default page-reload behavior and emits the submit event with the form already validated. For Template-driven forms, you’ll almost always use (ngSubmit).

Validation — through HTML5 attributes #

The biggest selling point of Template-driven Forms is that validation is written as HTML5 attributes. Standard attributes like required, minlength, maxlength, pattern, email are picked up by Angular as validation rules.

src/app/signup-form.component.html
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
  <input
    type="text"
    name="username"
    [(ngModel)]="model.username"
    required
    minlength="3"
  />

  <input
    type="email"
    name="email"
    [(ngModel)]="model.email"
    required
    email
  />

  <input
    type="password"
    name="password"
    [(ngModel)]="model.password"
    required
    pattern="^(?=.*[A-Za-z])(?=.*\d).{8,}$"
  />

  <button type="submit" [disabled]="userForm.invalid">Sign up</button>
</form>

The same validation in Reactive Forms would mean writing Validators.required, Validators.minLength(3), Validators.email, Validators.pattern(...) as arrays in the component class. In Template-driven, all of that fits in HTML attributes. For short forms, it really is convenient.

[disabled]="userForm.invalid" is also worth noting. While the form as a whole is invalid, this auto-disables the submit button. No separate boolean variable, no helper method needed.

Form state and error display #

Each control on the form carries the following state flags.

  • valid / invalid — whether validation passes
  • pristine / dirty — whether the user has changed the value at least once
  • untouched / touched — whether the user has focused and then blurred the field at least once

Use these flags to build the natural UX of “show errors only after the user has left the field.”

src/app/signup-form.component.html
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
  <label>
    Email
    <input
      type="email"
      name="email"
      [(ngModel)]="model.email"
      #emailCtrl="ngModel"
      required
      email
    />
  </label>

  @if (emailCtrl.invalid && emailCtrl.touched) {
    <p class="error">
      @if (emailCtrl.errors?.['required']) {
        Please enter your email.
      }
      @if (emailCtrl.errors?.['email']) {
        That doesn't look like a valid email.
      }
    </p>
  }

  <button type="submit" [disabled]="userForm.invalid">Sign up</button>
</form>

#emailCtrl="ngModel" is the pattern for grabbing a directive instance not for the whole form but for an individual control. With that handle you can directly access that one control’s valid, touched, errors, etc.

If you want to access it from the whole-form side, you can also reach in with userForm.controls['email']?.errors. Once you have many controls, the latter sometimes reads cleaner.

Reactive vs Template-driven #

Now let’s place the two approaches side by side.

AspectTemplate-drivenReactive
Where the form is definedHTML templateComponent class
Core toolsngModel, ngForm, HTML5 attributesFormGroup, FormControl, Validators
Module to importFormsModuleReactiveFormsModule
Learning curveLow — HTML-friendlyMedium — comfortable with objects and Observables
Dynamic forms (add/remove fields)AwkwardVery natural (FormArray)
Unit testingHeavier — needs a templateJust the class is enough — lightweight
Async validationPossible but awkwardIntegrates naturally
Subscribing to value changesAwkwardClean via valueChanges Observable

A rough summary:

  • Template-driven: when the form is simple, HTML5-level validation is enough, and you’d like “the whole form to fit on one screen of HTML.”
  • Reactive: when the form is complex, when fields grow and shrink based on input, when other parts of the page need to react to value changes, or when unit testing matters.

Practical recommendation — which one should you pick? #

For a brand-new project, the general advice is default to Reactive Forms. The code looks slightly heavier at first, but as the form grows the value of that structure shows up. Validation rules live together in one place inside the class, so they’re easy to track; adding a new field has a clear, small set of edits; and unit tests can rest cleanly on the component class alone.

Tip
Decision rule, in one line: if there’s any chance the form will grow, start with Reactive. Reach for Template-driven only when you can confidently say “this form will live and die as two inputs.” Mixing the two approaches across the app is fine; mixing them within a single form is the thing to avoid.

That doesn’t mean Template-driven is outdated or pointless. For forms with one or two inputs and lightweight validation — header search boxes, newsletter sign-ups, comment forms — Reactive Forms boilerplate is overkill. For those cases, finishing the job in a few HTML lines with Template-driven is much more natural.

Also, in environments where you collaborate with designers or markup-heavy contributors, the fact that they can directly tweak validation attributes in HTML is a real advantage. Keep in mind that the right answer can shift depending on team composition and project nature.

Wrapping up #

In this post we covered how Template-driven Forms work and compared them to Reactive Forms. To summarize:

  • Template-driven builds the form template-first. ngModel and HTML5 attributes are the core.
  • Inside a form, ngModel requires a name attribute.
  • Use #userForm="ngForm" for the whole form, #ctrl="ngModel" for an individual control.
  • Simple, lightweight forms → Template-driven; complex, dynamic, test-heavy forms → Reactive.
  • The default for new projects is Reactive first; reach for Template-driven only for genuinely simple forms.

In the next post, “Angular Intermediate #3 RxJS Basics,” we cover Angular’s other big pillar — RxJS. What Observable is, why Angular brought it in for async flows like HTTP responses and router parameters, and the most-used operators you should have in muscle memory.

X