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.