Angular Forms
Understand how Angular captures user input, validates data, tracks form state, and builds forms using template-driven, reactive, and signal-based approaches.
Lesson content
What are Angular Forms? Angular Forms provide tools for collecting, validating, tracking, and submitting user input. Forms are commonly used for: Login and registration Profile management Search and filtering Checkout and payment Admin data-entry screens Dynamic business forms Form Flow User Enters Data ↓ Angular Form Control ↓ Validation ↓ Form State Updated ↓ Submit ↓ Component or Service ↓ Backend API Angular Form Approaches Angular provides three approaches for building forms: Approach Best Used For Template-Driven Forms Small and simple forms Reactive Forms Complex, scalable, and testable forms Signal Forms Signal-based applications requiring typed form models and schema validation 1. Template-Driven Forms Template-driven forms define most form logic inside the HTML template. Angular creates and manages the underlying form controls using directives such as ngModel . Required Import import { FormsModule } from '@angular/forms'; Template-Driven Form Example import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; @Component({ selector: 'app-login', standalone: true, imports: [ FormsModule ], template: ` <form #loginForm="ngForm" (ngSubmit)="login()" > <label>Email</label> <input type="email" name="email" [(ngModel)]="user.email" required email #emailControl="ngModel" /> @if ( emailControl.invalid && emailControl.touched ) { <p> Enter a valid email. </p> } <label>Password</label> <input type="password" name="password" [(ngModel)]="user.password" required minlength="6" /> <button type="submit" [disabled]="loginForm.invalid" > Login </button> </form> ` }) export class LoginComponent { user = { email: '', password: '' }; login() { console.log(this.user); } } Template-Driven Form Flow HTML Input ↓ ngModel ↓ Component Property ↓ Validation Directives ↓ ngForm ↓ Submit ngModel ngModel connects an input with a component property. <input name="username" [(ngModel)]="username" /> The [()] syntax provides two-way binding. Input Changes ↓ Component Property Changes Component Property Changes ↓ Input Changes ngForm Angular automatically creates an NgForm instance for a form when FormsModule is available. <form #userForm="ngForm"> The form reference provides access to: value valid invalid dirty pristine touched submitted 2. Reactive Forms Reactive forms define the form model explicitly inside the TypeScript class. They provide direct access to form controls, synchronous state access, typed form models, reusable validation, and easier testing. Required Import import { ReactiveFormsModule } from '@angular/forms'; Basic Reactive Form import { Component, inject } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; @Component({ selector: 'app-register', standalone: true, imports: [ ReactiveFormsModule ], template: ` <form [formGroup]="registerForm" (ngSubmit)="submit()" > <label>Name</label> <input formControlName="name" /> @if ( registerForm.controls.name.invalid && registerForm.controls.name.touched ) { <p> Name is required. </p> } <label>Email</label> <input type="email" formControlName="email" /> <label>Password</label> <input type="password" formControlName="password" /> <button type="submit" [disabled]="registerForm.invalid" > Register </button> </form> ` }) export class RegisterComponent { private formBuilder = inject(FormBuilder); registerForm = this.formBuilder.nonNullable.group({ name: [ '', [ Validators.required, Validators.minLength(2) ] ], email: [ '', [ Validators.required, Validators.email ] ], password: [ '', [ Validators.required, Validators.minLength(8) ] ] }); submit() { if (this.registerForm.invalid) { this.registerForm.markAllAsTouched(); return; } console.log( this.registerForm.getRawValue() ); } } Reactive Form Building Blocks Class Purpose FormControl Manages one form field FormGroup Manages a group of named controls FormArray Manages a dynamic list of controls FormBuilder Simplifies form creation Validators Provides built-in validation functions ControlValueAccessor Connects custom controls with Angular Forms FormControl A FormControl manages the value and validation state of one field. import { FormControl, Validators } from '@angular/forms'; email = new FormControl( '', { nonNullable: true, validators: [ Validators.required, Validators.email ] } ); Template <input type="email" [formControl]="email" /> FormGroup A FormGroup combines related form controls into one object. import { FormControl, FormGroup } from '@angular/forms'; profileForm = new FormGroup({ firstName: new FormControl('', { nonNullable: true }), lastName: new FormControl('', { nonNullable: true }) }); Form Value { firstName: 'Ajay', lastName: 'Rajpoot' } Nested FormGroup addressForm = this.formBuilder.nonNullable.group({ name: [''], address: this.formBuilder.nonNullable.group({ city: [''], state: [''], postalCode: [''] }) }); Template <form [formGroup]="addressForm"> <input formControlName="name" /> <div formGroupName="address"> <input formControlName="city" /> <input formControlName="state" /> <input formControlName="postalCode" /> </div> </form> FormArray A FormArray manages a dynamic collection of controls. Dynamic Skills Example import { FormArray, FormControl } from '@angular/forms'; skills = new FormArray<FormControl<string>>([ new FormControl( '', { nonNullable: true } ) ]); addSkill() { this.skills.push( new FormControl( '', { nonNullable: true } ) ); } removeSkill(index: number) { this.skills.removeAt(index); } Template <div [formGroup]="profileForm"> <div formArrayName="skills"> @for ( control of skills.controls; track control; let index = $index ) { <input [formControlName]="index" /> <button type="button" (click)="removeSkill(index)" > Remove </button> } </div> </div> Built-In Validators Validator Purpose Validators.required Requires a value Validators.email Validates email format Validators.minLength() Requires a minimum length Validators.maxLength() Limits maximum length Validators.min() Requires a minimum numeric value Validators.max() Limits maximum numeric value Validators.pattern() Validates against a regular expression Custom Validator A custom validator returns null when valid or a validation-error object when invalid. import { AbstractControl, ValidationErrors } from '@angular/forms'; export function noSpacesValidator( control: AbstractControl ): ValidationErrors | null { const value = String(control.value ?? ''); return value.includes(' ') ? { spacesNotAllowed: true } : null; } Use the Validator username: [ '', [ Validators.required, noSpacesValidator ] ] Cross-Field Validation Cross-field validation checks related controls inside a form group. Password Match Validator import { AbstractControl, ValidationErrors } from '@angular/forms'; export function passwordMatchValidator( control: AbstractControl ): ValidationErrors | null { const password = control.get('password')?.value; const confirmPassword = control.get('confirmPassword')?.value; return password === confirmPassword ? null : { passwordMismatch: true }; } Apply to FormGroup passwordForm = this.formBuilder.nonNullable.group( { password: [''], confirmPassword: [''] }, { validators: [ passwordMatchValidator ] } ); Async Validator An asynchronous validator returns a Promise or Observable. It is commonly used to check whether a username or email already exists. import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms'; import { map, Observable } from 'rxjs'; export function usernameAvailableValidator( userService: UserService ): AsyncValidatorFn { return ( control: AbstractControl ): Observable<ValidationErrors | null> => { return userService .usernameExists(control.value) .pipe( map(exists => exists ? { usernameTaken: true } : null ) ); }; } Form State State Meaning valid All validation rules pass invalid One or more validation rules fail touched User focused and left the field untouched User has not left the field dirty User changed the value pristine User has not changed the value pending Async validation is running disabled Control is excluded from normal form value and validation valueChanges and statusChanges Reactive forms expose Observable streams for value and status changes. this.registerForm .valueChanges .subscribe(value => { console.log(value); }); this.registerForm .statusChanges .subscribe(status => { console.log(status); }); Search Form Example results$ = this.searchControl .valueChanges .pipe( debounceTime(300), distinctUntilChanged(), switchMap(term => this.productService .searchProducts(term) ) ); setValue() vs patchValue() setValue() patchValue() Requires values for every control Updates only provided controls Strict structure checking Allows partial updates Useful for complete replacement Useful for editing existing data setValue() this.profileForm.setValue({ firstName: 'Ajay', lastName: 'Rajpoot' }); patchValue() this.profileForm.patchValue({ firstName: 'Ajay' }); value vs getRawValue() value getRawValue() Excludes disabled controls Includes disabled controls May contain partial values Returns the complete raw form value Resetting a Form this.registerForm.reset(); Reset with Default Values this.registerForm.reset({ name: '', email: '', password: '' }); Typed Reactive Forms Reactive forms support strongly typed controls and form values. interface LoginForm { email: FormControl<string>; password: FormControl<string>; } loginForm = new FormGroup<LoginForm>({ email: new FormControl('', { nonNullable: true }), password: new FormControl('', { nonNullable: true }) }); Use non-nullable controls when null is not a valid form value. 3. Signal Forms Signal Forms use a writable signal as the source of truth for form data. They provide typed field access, automatic synchronization, schema-based validation, and signal-based field state. Basic Signal Form import { Component, signal } from '@angular/core'; import { email, form, FormField, required } from '@angular/forms/signals'; @Component({ selector: 'app-signal-login', standalone: true, imports: [ FormField ], template: ` <form (submit)="submit($event)"> <label>Email</label> <input type="email" [formField]="loginForm.email" /> @if ( loginForm.email().touched() && loginForm.email().invalid() ) { @for ( error of loginForm.email().errors(); track error.kind ) { <p> {{ error.message }} </p> } } <label>Password</label> <input type="password" [formField]="loginForm.password" /> <button type="submit" [disabled]="loginForm().invalid()" > Login </button> </form> ` }) export class SignalLoginComponent { loginModel = signal({ email: '', password: '' }); loginForm = form( this.loginModel, schema => { required( schema.email, { message: 'Email is required.' } ); email( schema.email, { message: 'Enter a valid email.' } ); required( schema.password, { message: 'Password is required.' } ); } ); submit(event: Event) { event.preventDefault(); console.log( this.loginModel() ); } } Signal Form Flow Writable Signal Model ↓ form() ↓ Signal Form Fields ↓ FormField Directive ↓ HTML Inputs ↓ Automatic Two-Way Synchronization Signal Forms vs model() A Signal Form model is a writable signal that stores form data. The model() API is different. It creates a component input and output for two-way component communication. Signal Form Model = Form Data State model() = Component Two-Way Binding Form Approaches Comparison Feature Template-Driven Reactive Signal Forms Source of truth Component model and template directives FormControl and FormGroup Writable signal model Setup location Mainly template Component class Signal model and schema Best for Simple forms Complex and enterprise forms New signal-based applications Validation Template directives Validator functions Schema-based validators State changes Two-way binding Observable-based form model Signals Testing More template-dependent Easy and explicit Direct signal state testing Custom Form Controls Use ControlValueAccessor when a custom component must behave like an Angular form control. Angular Form ↓ ControlValueAccessor ↓ Custom Component ↓ User Input ↓ Angular Form Updated A custom form control commonly implements: writeValue() registerOnChange() registerOnTouched() setDisabledState() Form Submission Flow User Clicks Submit ↓ Check Form Validity ↓ Invalid? ┌────┴────┐ │ │ Yes No │ │ ▼ ▼ Show Errors Read Form Value ↓ Call Service ↓ Send API Request ↓ Handle Response Common Mistakes 1. Submitting an Invalid Form if (this.form.invalid) { this.form.markAllAsTouched(); return; } 2. Mixing Form Approaches Avoid using ngModel and reactive form directives on the same control. 3. Using Any for Form Values Use typed controls and interfaces instead of any . 4. Forgetting Disabled Controls Use getRawValue() when disabled values must be included. 5. Placing Business Logic in Validators Validators should validate values. Move unrelated business operations into services. 6. Showing Errors Too Early Display validation errors after a control is touched, dirty, or after submission. 7. Trusting Frontend Validation Frontend validation improves user experience but does not replace backend validation. Angular Validation = User Experience Backend Validation = Data Security and Integrity Best Practices Use template-driven forms for small and straightforward forms Use reactive forms for complex, dynamic, and enterprise forms Consider Signal Forms for new signal-based form workflows Use typed and non-nullable form controls Keep validation functions reusable and focused Use FormArray for dynamic collections Use cross-field validators for related values Use async validators only when necessary Display accessible validation messages Disable repeated submissions while a request is pending Always validate submitted data on the backend Interview Questions What are Angular Forms? Angular Forms provide APIs for collecting user input, tracking control state, validating values, and submitting form data. What are the main form approaches? Angular supports template-driven forms, reactive forms, and signal-based forms. What is the difference between template-driven and reactive forms? Template-driven forms define most logic through template directives, while reactive forms explicitly define the form model in TypeScript. What is FormControl? A FormControl tracks the value, validation status, and interaction state of one form field. What is FormGroup? A FormGroup manages a collection of named form controls as one form model. What is FormArray? A FormArray manages a dynamic ordered collection of controls or form groups. What is the difference between setValue and patchValue? setValue() requires a complete matching form structure, while patchValue() updates only the supplied controls. What is an async validator? An async validator returns a Promise or Observable and is commonly used for server-based checks such as username availability. What is ControlValueAccessor? ControlValueAccessor connects a custom component with Angular's forms API. Does frontend form validation provide security? No. Frontend validation improves usability, but the backend must validate all submitted data independently. Key Point Angular Forms connect user input with a form model, validation rules, form state, and submission logic. Easy Formula Template-Driven Form = Template + ngModel + Directives Reactive Form = FormControl + FormGroup + Validators Signal Form = Writable Signal + form() + Schema + FormField Input ↓ Form Control ↓ Validation ↓ Form State ↓ Submit ↓ Service ↓ Backend