typescript
73 lines · 9 steps
How template-driven forms validate in Angular
An Angular signup form binds fields with ngModel and drives validation and submission from template references.
Explained by
highlit
1import { Component } from '@angular/core';
2import { NgForm } from '@angular/forms';
3
4interface SignupModel {
5 fullName: string;
6 email: string;
7 age: number | null;
8}
9
10@Component({
11 selector: 'app-signup-form',
12 template: `
13 <form #signupForm="ngForm" (ngSubmit)="onSubmit(signupForm)" novalidate>
14 <label>
15 Full name
16 <input
17 name="fullName"
18 [(ngModel)]="model.fullName"
19 #fullName="ngModel"
20 required
21 minlength="3" />
22 </label>
23 <div class="error" *ngIf="fullName.invalid && (fullName.dirty || fullName.touched)">
24 <small *ngIf="fullName.errors?.['required']">Name is required.</small>
25 <small *ngIf="fullName.errors?.['minlength']">At least 3 characters.</small>
26 </div>
27
28 <label>
29 Email
30 <input
31 name="email"
32 type="email"
33 [(ngModel)]="model.email"
34 #email="ngModel"
35 required
36 email />
37 </label>
38 <div class="error" *ngIf="email.invalid && email.touched">
39 <small *ngIf="email.errors?.['required']">Email is required.</small>
40 <small *ngIf="email.errors?.['email']">Enter a valid email address.</small>
41 </div>
42
43 <label>
44 Age
45 <input
46 name="age"
47 type="number"
48 [(ngModel)]="model.age"
49 #age="ngModel"
50 required
51 min="18" />
52 </label>
53 <div class="error" *ngIf="age.invalid && age.touched">
54 <small *ngIf="age.errors?.['required']">Age is required.</small>
55 <small *ngIf="age.errors?.['min']">You must be at least 18.</small>
56 </div>
57
58 <button type="submit" [disabled]="signupForm.invalid">Create account</button>
59 </form>
60 `,
61})
62export class SignupFormComponent {
63 model: SignupModel = { fullName: '', email: '', age: null };
64
65 onSubmit(form: NgForm): void {
66 if (form.invalid) {
67 form.control.markAllAsTouched();
68 return;
69 }
70 console.log('Submitting', this.model);
71 form.resetForm();
72 }
73}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Template-driven forms let the template own validation state via exported ngModel and ngForm references.
- 2Two-way ngModel binding keeps a typed model object in sync with the inputs automatically.
- 3Guarding error messages behind dirty or touched prevents warnings from flashing before the user interacts.
Related explainers
ruby
require "phonelib" class PhoneNumber class InvalidNumber < StandardError; end
Wrapping phone parsing in a Ruby value object
value-object
memoization
validation
Intermediate
7 steps
typescript
import { Injectable, signal, computed } from '@angular/core'; export type ToastKind = 'success' | 'error' | 'info' | 'warning';
Building a signal-based toast service in Angular
signals
state-management
dependency-injection
Intermediate
8 steps
python
from flask import Blueprint, request, jsonify from marshmallow import Schema, fields, validate, ValidationError, EXCLUDE from .models import db, User
How a Flask blueprint validates and creates users
validation
rest-api
schema
Intermediate
8 steps
typescript
import { Controller } from '@nestjs/common'; import { MessagePattern, Payload,
Manual RabbitMQ acks in a NestJS controller
microservices
message-queue
acknowledgement
Intermediate
8 steps
typescript
type LazyImageOptions = { rootMargin?: string; loadedClass?: string; };
Lazy-loading images with IntersectionObserver
intersectionobserver
lazy-loading
performance
Intermediate
7 steps
typescript
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
Type-safe deep merge in TypeScript
recursion
conditional-types
mapped-types
Advanced
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-template-driven-forms-validate-in-angular-explained-typescript-5cea/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.