Angular – Template-Driven Forms

July 30, 20262 min readUpdated 8/21/2026

Angular has two form systems. This is the smaller one: the form lives in the template, and Angular builds the model behind it from what it finds there.

<form (ngSubmit)="submit()">
  <div class="mb-3">
    <label class="form-label" for="login-email">Email</label>
    <input
      id="login-email"
      name="email"
      class="form-control"
      type="email"
      required
      autocomplete="username"
      appAutofocus
      [ngModel]="email()"
      (ngModelChange)="email.set($event)"
    />
  </div>

Three pieces are doing work. ngModel binds the control to a value. name is required — it is the key the parent form registers the control under, and without it Angular throws. required is the standard HTML attribute, which Angular also reads as a validator.

It needs FormsModule in the component's imports.

Why this one is written the long way

The usual spelling is the banana in a box, [(ngModel)]="email". The pizza app splits it:

[ngModel]="email()"
(ngModelChange)="email.set($event)"

Because the field is a signal. [(ngModel)] expands to an assignment — email = $event — and a signal is not an assignment target; you set it by calling set(). The long form is the same binding written out, and it is what you need whenever the backing value is a signal rather than a plain property.

The form's state

Angular tracks each control and the form as a whole. A template reference variable bound to ngForm exposes it:

<form #loginForm="ngForm" (ngSubmit)="submit()">
  <button type="submit" [disabled]="loginForm.invalid">Sign in</button>
</form>

Per control: valid / invalid, pristine / dirty (has the value been changed), untouched / touched (has it been focused and blurred), and errors.

The distinction that matters is dirty versus touched. Showing "email is required" before someone has typed anything is the most common form mistake there is. The fix is to gate the message on touched — the field has been visited and left — rather than on invalid alone:

@if (emailField.touched && emailField.invalid) {
  <div class="invalid-feedback d-block">A valid email is required.</div>
}

Validators

Template-driven forms use HTML attributes: required, minlength, maxlength, pattern, min, max, email. That is a real limitation — an attribute cannot express "required only when the order is a delivery". Cross-field and conditional rules are where this system stops being the right tool.

Which to use

The pizza app has seven forms, and two are template-driven: login and register. That split is deliberate rather than inconsistent.

Template-driven is the right size for a couple of fields with no cross-field rules, nothing to patch in from a server, and no per-field errors to attach after a failed submit. Login is exactly that.

Reactive earns its keep the moment a form has real validation to express, values arriving from an API, or server errors to map back onto controls. Checkout, profile and the admin editors are all that.

The honest summary: reactive is the default for anything non-trivial, and template-driven is worth knowing because small forms are common and it is genuinely less code for them.

What is next

The other system — forms defined in the class, where they can be typed and tested.