Published: December 19 2022

Angular 14 - Template-Driven Form Validation Example

Example built with Angular 14.2.12

Other versions available:

This is a quick example of how to implement form validation in Angular 14 with Template-Driven Forms.

Template-Driven vs Reactive Forms

With template-driven forms pretty much everything is defined in the template (as the name suggests). Form data is accessed via a template variable on the form element (e.g. <form #f="ngForm" ...>), the ngModel directive is used to register form input controls with the parent form (e.g. <input name="email" ngModel ... >), directives are used to specify validation rules on each input element (e.g. <input ... required email>), and model properties such as validation error messages (e.g. email.errors) are accessed via template variables declared on the input element (e.g. <input ... #email="ngModel">).

Alternatively, reactive forms use a model-driven approach where you define your form fields and validation rules in the component as a FormGroup and bind it to the form element in the template with a data-binding attribute (e.g. <form ... [formGroup]="registerForm">). For the same tutorial built with reactive forms see Angular 14 - Reactive Forms Validation Example.

Example Angular 14 Registration Form

The example is a simple registration form with pretty standard fields for title, first name, last name, date of birth, email, password, confirm password and an accept terms and conditions checkbox. The form defines the following validation rules:

  • Required fields - all fields are required including the checkbox.
  • Date validation - the DOB field must contain a valid date.
  • Email address validation - the email address must be in a valid format.
  • Password min length - the password must contain at least 6 characters.
  • Passwords must match - the confirm password and password fields must match. This is implemented by a custom MustMatch validator and attached to the form with a custom [mustMatch] directive.

Validate on Form Submit

I setup the form to validate on submit rather than as soon as each field is changed (touched/dirty), this is implemented using the f.submitted property of the form template variable (#f="ngForm"), the property equals true after the form is submitted for the first time.

Styled with Bootstrap 5

The example login app is styled with the CSS from Bootstrap 5.2, for more info about Bootstrap see

Code on GitHub

The project is available on github at

Here it is in action: (See on StackBlitz at

App Component that handles Form Data

The component doesn't do much when using angular template-driven forms since the form fields and validators are defined in the component template.

The onSubmit() method is called with the NgForm template variable when the form is submitted, it is bound to the form element in the template using Angular event binding syntax ((ngSubmit)="onSubmit(f)"). If valid a simple javascript alert is displayed with the values entered into the form.

import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';

@Component({ selector: 'app-root', templateUrl: 'app.component.html' })
export class AppComponent {
    onSubmit(f: NgForm) {
        // stop here if form is invalid
        if (f.invalid) {

        alert('SUCCESS!! :-)\n\n' + JSON.stringify(f.value, null, 4));

App Component Template with Form Validation Rules

The app component template contains all the html markup for displaying the example registration form in the browser. The form submit event is bound to the onSubmit() method of the app component with the event binding (ngSubmit)="onSubmit(f)". The template variable #f="ngForm" creates a FormGroup instance to provide access to the form data and validation status.

Input fields registered with parent form

Each input field is registered with the form using the ngModel directive. In the context of a parent form the directive can be used without data binding ([()]), instead the control is registered using the input name attribute and the form keeps the data in sync.

Validators and directives

Validation is implemented by validator functions that are attached to fields using directives. Most of the validators used come as part of the Angular framework (required, minlength, email, pattern). One custom validator (mustMatch) was created to check the password and confirm password fields match.

Date input validation

The pattern directive is used with a regular expression (^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$) to validate the format of the date field (yyyy-mm-dd).

<!-- main app container -->
<div class="card m-3">
    <h5 class="card-header text-center">Angular 14 Template-Driven Form Validation</h5>
    <div class="card-body">
        <form #f="ngForm" (ngSubmit)="onSubmit(f)" [mustMatch]="['password', 'confirmPassword']" novalidate>
            <div class="row">
                <div class="col mb-3">
                    <label class="form-label">Title</label>
                    <select name="title" ngModel #title="ngModel" class="form-control" [ngClass]="{ 'is-invalid': f.submitted && title.invalid }" required>
                        <option value=""></option>
                        <option value="Mr">Mr</option>
                        <option value="Mrs">Mrs</option>
                        <option value="Miss">Miss</option>
                        <option value="Ms">Ms</option>
                    <div *ngIf="f.submitted && title.errors" class="invalid-feedback">
                        <div *ngIf="title.errors.required">Title is required</div>
                <div class="col-5 mb-3">
                    <label class="form-label">First Name</label>
                    <input type="text" name="firstName" ngModel #firstName="ngModel" class="form-control" [ngClass]="{ 'is-invalid': f.submitted && firstName.invalid }" required>
                    <div *ngIf="f.submitted && firstName.errors" class="invalid-feedback">
                        <div *ngIf="firstName.errors.required">First Name is required</div>
                <div class="col-5 mb-3">
                    <label class="form-label">Last Name</label>
                    <input type="text" name="lastName" ngModel #lastName="ngModel" class="form-control" [ngClass]="{ 'is-invalid': f.submitted && lastName.invalid }" required>
                    <div *ngIf="f.submitted && lastName.errors" class="invalid-feedback">
                        <div *ngIf="lastName.errors.required">Last Name is required</div>
            <div class="row">
                <div class="col mb-3">
                    <label class="form-label">Date of Birth</label>
                    <input type="date" name="dob" ngModel #dob="ngModel" class="form-control" [ngClass]="{ 'is-invalid': f.submitted && dob.invalid }" required pattern="^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$">
                    <div *ngIf="f.submitted && dob.errors" class="invalid-feedback">
                        <div *ngIf="dob.errors.required">Date of Birth is required</div>
                        <div *ngIf="dob.errors.pattern">Date of Birth must be a valid date in the format YYYY-MM-DD</div>
                <div class="col mb-3">
                    <label class="form-label">Email</label>
                    <input type="text" name="email" ngModel #email="ngModel" class="form-control" [ngClass]="{ 'is-invalid': f.submitted && email.invalid }" required email>
                    <div *ngIf="f.submitted && email.errors" class="invalid-feedback">
                        <div *ngIf="email.errors.required">Email is required</div>
                        <div *ngIf="">Email must be a valid email address</div>
            <div class="row">
                <div class="col mb-3">
                    <label class="form-label">Password</label>
                    <input type="password" name="password" ngModel #password="ngModel" class="form-control" [ngClass]="{ 'is-invalid': f.submitted && password.invalid }" required minlength="6">
                    <div *ngIf="f.submitted && password.errors" class="invalid-feedback">
                        <div *ngIf="password.errors.required">Password is required</div>
                        <div *ngIf="password.errors.minlength">Password must be at least 6 characters</div>
                <div class="col mb-3">
                    <label class="form-label">Confirm Password</label>
                    <input type="password" name="confirmPassword" ngModel #confirmPassword="ngModel" class="form-control" [ngClass]="{ 'is-invalid': f.submitted && confirmPassword.invalid }" required>
                    <div *ngIf="f.submitted && confirmPassword.errors" class="invalid-feedback">
                        <div *ngIf="confirmPassword.errors.required">Confirm Password is required</div>
                        <div *ngIf="confirmPassword.errors.mustMatch">Passwords must match</div>
            <div class="form-check mb-3">
                <input type="checkbox" name="acceptTerms" id="acceptTerms" ngModel #acceptTerms="ngModel" class="form-check-input" [ngClass]="{ 'is-invalid': f.submitted && acceptTerms.invalid }" required>
                <label for="acceptTerms" class="form-check-label">Accept Terms & Conditions</label>
                <div *ngIf="f.submitted && acceptTerms.errors" class="invalid-feedback">Accept Ts & Cs is required</div>
            <div class="text-center">
                <button class="btn btn-primary me-1">Register</button>
                <button class="btn btn-secondary" type="reset">Cancel</button>

Custom MustMatch Validator

The custom MustMatch validator is used in the example to validate that both of the password fields match. However it can be used to validate that any pair of fields is matching (e.g. email and confirm email fields).

It works slightly differently than a typical validator, usually a validator expects an input control parameter whereas this one expects a form group because it's validating two inputs instead of one.

Also instead of returning an error which would be attached to the parent form group, the validator calls matchingControl.setErrors() to attach the error to the confirm password field. I thought this made more sense because the validation error is displayed below the confirmPassword field in the template.

import { AbstractControl } from '@angular/forms';

// custom validator to check that two fields match
export function MustMatch(controlName: string, matchingControlName: string) {
    return (group: AbstractControl) => {
        const control = group.get(controlName);
        const matchingControl = group.get(matchingControlName);

        if (!control || !matchingControl) {
            return null;

        // return if another validator has already found an error on the matchingControl
        if (matchingControl.errors && !matchingControl.errors.mustMatch) {
            return null;

        // set error on matchingControl if validation fails
        if (control.value !== matchingControl.value) {
            matchingControl.setErrors({ mustMatch: true });
        } else {
        return null;

Custom mustMatch Directive

The custom [mustMatch] directive wraps the custom MustMatch validator so we can attach it to the form. A custom validator directive is required when using template-driven forms because we don't have direct access to the FormGroup like in reactive forms.

The directive implements the Validator interface and registers itself with the NG_VALIDATORS provider to let angular know that it's a custom validator directive.

It accepts an array with the names of 2 form controls that must match in order for form validation to pass, e.g. [mustMatch]="['field1', 'field2']" will validate that field1 and field2 contain the same value, otherwise a validation error will be set on field2. You can see it's usage in the form tag of the app template above.

import { Directive, Input } from '@angular/core';
import { NG_VALIDATORS, Validator, ValidationErrors, FormGroup } from '@angular/forms';

import { MustMatch } from './must-match.validator';

    selector: '[mustMatch]',
    providers: [{ provide: NG_VALIDATORS, useExisting: MustMatchDirective, multi: true }]
export class MustMatchDirective implements Validator {
    @Input('mustMatch') mustMatch: string[] = [];

    validate(formGroup: FormGroup): ValidationErrors | null {
        return MustMatch(this.mustMatch[0], this.mustMatch[1])(formGroup);

App Module

There isn't much going on in the app module other than the standard stuff, the main thing you need to remember for using template-driven forms in Angular is to import the FormsModule from '@angular/forms' and include it in the imports array of the @NgModule decorator. Also import the custom validation directive { MustMatchDirective } from './_helpers/must-match.directive' and include it in the declarations array of the @NgModule decorator.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { MustMatchDirective } from './_helpers';

    imports: [
    declarations: [
    bootstrap: [AppComponent]
export class AppModule { }


Need Some Angular 14 Help?

Search fiverr for freelance Angular 14 developers.

Follow me for updates

On Twitter or RSS.

When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Supported by