AngularJSForms Overview

Angular Forms Overview

Forms are central to almost every web application — login screens, search boxes, settings panels, checkout flows. Angular provides two powerful, built-in approaches for building forms:

  • Template-Driven Forms — declare form logic in HTML with directives; minimal TypeScript
  • Reactive Forms — declare form logic entirely in TypeScript; HTML just connects to the model

Both approaches integrate with Angular's change detection, validation pipeline, and accessibility support. Choosing the right one depends on your form's complexity and your team's preferences.

Template-Driven Forms at a Glance

Template-driven forms use Angular directives (ngModel, ngForm, required, minlength, etc.) directly in the template. Angular creates the underlying form model automatically.

TS
// Template-driven — minimal component code
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #f="ngForm" (ngSubmit)="onSubmit(f.value)">
      <input name="email" ngModel required email />
      <button type="submit" [disabled]="f.invalid">Submit</button>
    </form>
  `,
})
export class LoginComponent {
  onSubmit(value: unknown) {
    console.log(value);
  }
}
Reactive Forms at a Glance

Reactive forms build the form model in the component class using FormGroup, FormControl, and FormArray. The template simply binds to these objects.

TS
// Reactive — form defined in TypeScript
import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';

@Component({
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <input formControlName="email" />
      <button type="submit" [disabled]="form.invalid">Submit</button>
    </form>
  `,
})
export class LoginComponent {
  form = new FormGroup({
    email: new FormControl('', [Validators.required, Validators.email]),
  });

  onSubmit() {
    console.log(this.form.value);
  }
}
Comparison: Template-Driven vs Reactive

Aspect

Template-Driven

Reactive

Form model location

Created by Angular from the template

Explicitly built in the component class

Data flow

Two-way binding with ngModel

Immutable observables via valueChanges

Validation

HTML attributes + directives

Validator functions in TypeScript

Dynamic fields

Difficult

Easy with FormArray

Unit testing

Requires DOM / Angular TestBed

Pure TypeScript, no DOM needed

Async validation

Supported but awkward

First-class support

Best for

Simple forms, prototypes

Complex, dynamic, or data-heavy forms

Import needed

FormsModule

ReactiveFormsModule

Core Building Blocks

Both form types share these foundational classes from @angular/forms:

FormControl — tracks the value and validation state of a single input element.

FormGroup — a collection of FormControl instances that tracks value and validity as a unit.

FormArray — an ordered list of controls whose length can change dynamically.

AbstractControl — the base class for all three; provides shared properties like value, valid, invalid, pristine, dirty, touched, and errors.

TS
import { FormGroup, FormControl, FormArray, Validators } from '@angular/forms';

// FormControl — single field
const nameControl = new FormControl('', Validators.required);
console.log(nameControl.value);   // ''
console.log(nameControl.valid);   // false (required + empty)

// FormGroup — object of controls
const addressGroup = new FormGroup({
  street: new FormControl(''),
  city:   new FormControl('', Validators.required),
  zip:    new FormControl('', Validators.pattern(/^\d{5}$/)),
});

// FormArray — dynamic list of controls
const tagsArray = new FormArray([
  new FormControl('angular'),
  new FormControl('typescript'),
]);
tagsArray.push(new FormControl('rxjs')); // add controls at runtime
Control State Properties

Angular tracks interaction state on every control. Use these to show errors only after the user has interacted with a field.

Property

Opposite

Meaning

valid

invalid

All validators pass / at least one fails

pristine

dirty

Value unchanged since init / value has been changed

untouched

touched

Field never blurred / field has been blurred

pending

Async validator is running

disabled

enabled

Control is disabled

HTML
<!-- Show error only after user interacts with the field -->
<input formControlName="email" />
<div *ngIf="form.get('email')?.invalid && form.get('email')?.touched">
  Please enter a valid email address.
</div>
The Validators Class

Angular's built-in Validators covers the most common validation rules. You can compose multiple validators using an array.

TS
import { Validators } from '@angular/forms';

// Single validator
new FormControl('', Validators.required)

// Multiple validators (array)
new FormControl('', [
  Validators.required,
  Validators.minLength(3),
  Validators.maxLength(50),
])

// Built-in validators
Validators.required         // value must be non-empty
Validators.email            // must match email format
Validators.min(0)           // numeric min
Validators.max(100)         // numeric max
Validators.minLength(8)     // string min length
Validators.maxLength(128)   // string max length
Validators.pattern(/regex/) // must match regex
Async Validators

For validations that require a network call — such as checking if a username is already taken — Angular supports async validators. They return a Promise or Observable that resolves to validation errors (or null for valid).

TS
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, catchError, debounceTime, switchMap, first } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class UsernameValidator {
  constructor(private http: HttpClient) {}

  checkAvailability(): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors | null> => {
      if (!control.value) return of(null);

      return of(control.value).pipe(
        debounceTime(400),
        switchMap(username =>
          this.http.get<{ available: boolean }>(`/api/users/check/${username}`)
        ),
        map(res => res.available ? null : { usernameTaken: true }),
        catchError(() => of(null)),
        first(),
      );
    };
  }
}

// Usage in component
username = new FormControl('', {
  validators: [Validators.required, Validators.minLength(3)],
  asyncValidators: [this.usernameValidator.checkAvailability()],
  updateOn: 'blur',  // only validate when focus leaves the field
});
Note
The third argument to new FormControl() is always the async validator (or an array of them). Async validators only run after all synchronous validators pass, saving unnecessary network requests.
Choosing Between the Two Approaches
  • Use template-driven forms for simple contact forms, login screens, or quick prototypes

  • Use reactive forms when you need dynamic field lists (FormArray), complex cross-field validation, or programmatic control

  • Reactive forms are easier to unit-test because the model is plain TypeScript objects

  • Both approaches can be used in the same application — pick per form, not per project

  • The Angular team considers reactive forms to be the "scalable" approach for enterprise apps

Tip
Angular 14+ introduced **typed reactive forms** where FormControl<string>, FormGroup<...>, and FormArray<...> carry full TypeScript generics. This eliminates a large class of runtime errors — always enable strict typing in new projects.
Module Setup

TS
// For template-driven forms
import { FormsModule } from '@angular/forms';

// For reactive forms
import { ReactiveFormsModule } from '@angular/forms';

// In a standalone component — import directly
@Component({
  standalone: true,
  imports: [FormsModule],    // or ReactiveFormsModule
  template: `...`,
})
export class MyFormComponent {}

// In an NgModule — import once in the module
@NgModule({
  imports: [FormsModule, ReactiveFormsModule],
})
export class AppModule {}

Summary: Angular offers two complementary form systems. Template-driven forms are quick and declarative; reactive forms are explicit and scalable. Both share the same FormControl/FormGroup/FormArray model and validation pipeline. The following pages dive deep into each approach, validation, custom validators, and more.