Angular Template-driven Forms: ngModel, ngForm and Form Validation
We build a template-driven form in Angular 12: FormsModule, ngModel and name, ngForm with ngSubmit, required/email/pattern validators, touched/dirty states, CSS classes and ngModelGroup.
Version note: The code and commands in this post are for Angular 12.
The Hidden Weight of a Form
At first glance, a form is just a few inputs and a button. Once you get into it, the list grows: collecting values, checking required fields, validating email format, showing error messages at the right moment, disabling the button while the form is invalid. Try to do all that by hand, wiring an event to every input, and the component quickly turns into a pile of if-else statements.
Think about filling in a form at the bank. You slide it across the counter, and the clerk spots the missing fields and the mistyped number at a glance and says "please fix this part." In Angular, that clerk has two ways of working: template-driven forms and reactive forms. In this post we'll cover the first one, where the rules are written right into the template. Our example will be a small event registration form.
Adding FormsModule
We imported FormsModule earlier for [(ngModel)]. All the tools for template-driven forms come from that same module:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { RegisterComponent } from './register/register.component';
@NgModule({
declarations: [AppComponent, RegisterComponent],
imports: [BrowserModule, FormsModule],
bootstrap: [AppComponent]
})
export class AppModule { }
The moment FormsModule is imported, two things happen behind the scenes. A directive called NgForm is automatically attached to every <form> tag in your templates and starts tracking the form's fields. On top of that, a novalidate attribute is added to every form. That switches off the browser's own validation bubbles and hands control entirely to Angular.
The First Form: ngModel and name
<form>
<label>Full name</label>
<input name="fullName" ngModel>
<label>Email</label>
<input name="email" ngModel>
<button type="submit">Register</button>
</form>
Here ngModel sits there as a bare attribute, without square brackets or parentheses. In this form it means "register this input as a field of the form." It isn't bound to any property in the class; the form holds the value itself.
The name attribute, on the other hand, is required. The form identifies fields by this name and stores the value under that key. Write ngModel without name and Angular throws an error saying that an ngModel inside a form needs either a name or to be marked as standalone. If you also want the value in a class property, you can write [(ngModel)]="fullName"; name still has to stay.
#f="ngForm" and ngSubmit
To get at the values when the form is submitted, we give the form a name:
<form #f="ngForm" (ngSubmit)="onSubmit(f)">
<input name="fullName" ngModel>
<input name="email" ngModel>
<button type="submit">Register</button>
</form>
<pre>{{ f.value | json }}</pre>
import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
@Component({
selector: 'app-register',
templateUrl: './register.component.html'
})
export class RegisterComponent {
onSubmit(form: NgForm): void {
console.log(form.value);
form.resetForm();
}
}
#f is the template reference variable we saw earlier. Normally it holds the DOM element itself, but the ="ngForm" part tells it "don't give me this tag's DOM element, give me the NgForm directive sitting on it." Through f we can now reach the form's value, its validity and whether it has been submitted.
(ngSubmit) fires when the user clicks the submit button or presses Enter inside an input, and the page doesn't reload. form.value gathers every field into a single object, using the name values as keys: { fullName: '...', email: '...' }. resetForm resets both the values and the state information we're about to see. The line with the json pipe at the bottom shows the form value changing live as you type; it's very handy during development.
Built-in Validator Attributes
We write the rules with familiar HTML attributes as well:
<input name="fullName" ngModel required minlength="3">
<input name="email" ngModel required email>
<input name="phone" ngModel pattern="5[0-9]{9}">
<input name="guests" type="number" ngModel min="1" max="5">
<button type="submit" [disabled]="f.invalid">Register</button>
These look like standard attributes, but FormsModule attaches a validator directive to each of them. required makes a field mandatory, while minlength and maxlength set character limits. email is Angular's own email check; there's no such attribute in standard HTML. pattern takes a regular expression. Angular adds ^ and $ to the start and end for you, so the phone field above requires exactly a 10-digit number starting with 5. On type="number" fields, min and max work as validators too.
The form's validity is the sum of all its fields. If even one field is invalid, f.invalid is true and we disable the button with [disabled].
Control States and CSS Classes
Besides the form, each field has its own state. To reach a field, we give it a reference too, this time with ngModel:
<input name="fullName" ngModel required minlength="3" #fullName="ngModel">
<p>valid: {{ fullName.valid }} | touched: {{ fullName.touched }} | dirty: {{ fullName.dirty }}</p>
There are three pairs of states. valid and invalid tell you whether the rules are met. touched and untouched track whether the user has entered and left the field, in other words whether it has lost focus. dirty and pristine show whether the user has changed the value. They're like the three questions running through the bank clerk's head: has this field been touched, has anything been changed, and is what's written correct?
Angular also adds these states to the elements as CSS classes: ng-valid or ng-invalid, ng-touched or ng-untouched, ng-dirty or ng-pristine. Inspect an input with the developer tools and you'll see the classes change as you type. That lets you give visual feedback without writing a single line of TypeScript:
input.ng-invalid.ng-touched {
border: 1px solid crimson;
}
input.ng-valid.ng-dirty {
border: 1px solid seagreen;
}
Why two classes at once? With ng-invalid alone, every required field would turn red the moment the page opens. Showing users errors before they've done anything is rude. The ng-touched condition holds the red back until the user has left the field.
Showing Error Messages
A red border says something is wrong, but not what. For that, we combine the field reference with *ngIf:
<input name="email" ngModel required email #email="ngModel">
<div *ngIf="email.invalid && (email.dirty || email.touched || f.submitted)" class="error">
<p *ngIf="email.hasError('required')">Email is required.</p>
<p *ngIf="email.hasError('email')">Please enter a valid email address.</p>
</div>
The outer condition decides when the message appears: when the field is invalid and the user has touched it or tried to submit the form. f.submitted makes errors show up even when the user clicks the button without touching any field. For that, the button must not be disabled; if you've disabled it with [disabled], this condition never gets its turn.
The inner conditions check which rule was broken. Every field has an errors object. When the field is valid, it's null; otherwise it carries a key for each broken rule, like { required: true }. hasError is the most readable, safe way to look into that object. Some errors carry extra information:
<p *ngIf="fullName.hasError('minlength')">
Please enter at least {{ fullName.getError('minlength').requiredLength }} characters.
</p>
The minlength error carries an object like { requiredLength: 3, actualLength: 1 }. getError returns that object, and we put the limit into the message. That way, when you change the rule, you don't have to update the message separately.
Grouping Fields with ngModelGroup
Gathering related fields, like an address, under one group both tidies up the value and lets you check the group as a single unit:
<div ngModelGroup="address" #address="ngModelGroup">
<input name="city" ngModel required placeholder="City">
<input name="zip" ngModel pattern="[0-9]{5}" placeholder="Postal code">
</div>
<p *ngIf="address.invalid && address.touched">Please check your address details.</p>
When the form is submitted, the value is now a nested object:
{
"fullName": "Elif Kaya",
"email": "elif@example.com",
"phone": "5321234567",
"address": {
"city": "İzmir",
"zip": "35000"
}
}
ngModelGroup combines the state of the fields inside it. If a single field in the group is invalid, the whole group counts as invalid. If your API already expects data in this shape, you can send the form value as is, without any extra mapping.
A Small Experiment
Your turn. Build an event registration form: full name (required, at least 3 characters), email (required, valid format), phone (10 digits with pattern) and number of guests (type="number", min="1", max="5"). Put city and postal code inside an ngModelGroup.
Show each field's error messages only after the user has touched it, and change the border color with the ng-invalid and ng-touched classes. Put {{ f.value | json }} below the form and watch the value take shape. Clear the form with resetForm after submitting. Finally, delete the name attribute from one input and read the error in the console.
You may have noticed that all the rules live in the template. As the form grows, as fields are added and removed dynamically, or when two fields need to be checked against each other, this approach starts to struggle. In the next post we'll talk about reactive forms, which move control of the form into the class.