Angular Forms: Reactive & Template-Driven Validation

Blog / Angular · June 19, 2017 · Updated June 10, 2026 · 7 min read
Angular Forms: Reactive & Template-Driven Validation

Angular gives you two ways to build forms: template-driven forms (simple, HTML-first, powered by FormsModule and [(ngModel)]) and reactive forms (explicit, type-safe, powered by ReactiveFormsModule and FormGroup/FormControl). For anything beyond a trivial input — dynamic fields, cross-field rules, async checks, or unit tests — use reactive forms; reach for template-driven only for small, static forms.

This guide covers both approaches in modern Angular (17/18/19), using standalone components, typed reactive forms, and built-in, custom, and async validators.

Key takeaways

  • Two APIs, one goal: template-driven (declarative, lives in the template) vs reactive (programmatic, lives in the component class).
  • Default to reactive forms for non-trivial UIs — they are explicit, strongly typed, and easy to unit test without a DOM.
  • Typed reactive forms (Angular 14+) catch wrong field names and value types at compile time; pair them with nonNullable.
  • Standalone components are the default since Angular 17 — import ReactiveFormsModule (or FormsModule) directly in the component, no NgModule required.
  • Validation = built-in validators (Validators.required, email, minLength, pattern), custom synchronous ValidatorFns, async validators, and cross-field validators on the FormGroup.
  • Angular is also building signal-based forms, but both existing form APIs remain fully supported.

What are template-driven and reactive forms?

Template-driven forms put the logic in the template. You add [(ngModel)] bindings and validation attributes (required, minlength, email) to inputs, and Angular builds the underlying form model for you. Import FormsModule.

Reactive forms (also called model-driven forms) put the logic in the component class. You build an explicit tree of FormControl, FormGroup, and FormArray objects, then bind it to the template. Import ReactiveFormsModule.

Reactive forms vs template-driven forms

Aspect Template-driven Reactive
Where the model lives In the template ([(ngModel)]) In the component class (FormGroup)
Setup module FormsModule ReactiveFormsModule
Data flow Asynchronous, mutable Synchronous, immutable snapshots
Type safety Weak Strong (typed forms)
Dynamic fields Awkward First-class (FormArray)
Testability Needs the DOM Test the model directly, no DOM
Best for Small, static forms Complex, scalable, testable forms

How do you build a template-driven form?

Import FormsModule into your standalone component, bind each field with [(ngModel)], expose a template reference (#email="ngModel") to read validation state, and handle (ngSubmit).

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

@Component({
  selector: 'app-signup',
  standalone: true,
  imports: [FormsModule],
  templateUrl: './signup.component.html',
})
export class SignupComponent {
  model = { firstName: '', lastName: '', email: '' };

  onSubmit(form: NgForm): void {
    if (form.invalid) {
      return;
    }
    console.log('Submitted', this.model);
  }
}
<form #signupForm="ngForm" (ngSubmit)="onSubmit(signupForm)">
  <label>First name</label>
  <input name="firstName" [(ngModel)]="model.firstName" required #firstName="ngModel" />
  @if (firstName.touched && firstName.errors?.['required']) {
    <small>First name is required.</small>
  }

  <label>Email</label>
  <input name="email" type="email" [(ngModel)]="model.email" required email #email="ngModel" />
  @if (email.touched && email.invalid) {
    <small>Enter a valid email.</small>
  }

  <button type="submit" [disabled]="signupForm.invalid">Submit</button>
</form>

How do you build a typed reactive form?

Reactive forms shine when a form has rules. Import ReactiveFormsModule, build the model with FormBuilder (or FormGroup/FormControl directly), and let TypeScript infer a strongly typed model. Use nonNullable so controls reset to their initial value instead of null.

import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'app-signup',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './signup.component.html',
})
export class SignupComponent {
  private fb = inject(FormBuilder);

  // Strongly typed, non-nullable controls (Angular 14+).
  form = this.fb.nonNullable.group({
    firstName: ['', [Validators.required, Validators.minLength(2)]],
    lastName: ['', Validators.required],
    email: ['', [Validators.required, Validators.email]],
  });

  get email() {
    return this.form.controls.email;
  }

  onSubmit(): void {
    if (this.form.invalid) {
      this.form.markAllAsTouched();
      return;
    }
    // getRawValue() is typed: { firstName: string; lastName: string; email: string }
    console.log(this.form.getRawValue());
  }
}

With standalone components there is no AppModule. You start the app with bootstrapApplication in main.ts, and every component declares the form module it needs through its own imports array:

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent);
<form [formGroup]="form" (ngSubmit)="onSubmit()">
  <input formControlName="firstName" placeholder="First name" />
  <input formControlName="lastName" placeholder="Last name" />
  <input formControlName="email" type="email" placeholder="Email" />

  @if (email.touched && email.errors?.['required']) {
    <small>Email is required.</small>
  } @else if (email.touched && email.errors?.['email']) {
    <small>That email looks invalid.</small>
  }

  <button type="submit" [disabled]="form.invalid">Create account</button>
</form>

Which validators does Angular provide?

Angular ships built-in validators in the Validators class. Pass one — or several as an array — when you create a control.

Validator What it checks
Validators.required Field has a value
Validators.requiredTrue Checkbox is checked (true)
Validators.email Value is a valid email
Validators.minLength(n) / maxLength(n) String/array length bounds
Validators.min(n) / max(n) Numeric bounds
Validators.pattern(regex) Value matches a regular expression

When validation fails, the control's errors object holds a key per failed rule — for example { required: true } or { minlength: { requiredLength: 2, actualLength: 1 } }.

How do you write a custom validator?

A synchronous validator is a ValidatorFn: a function that takes an AbstractControl and returns a ValidationErrors object when invalid, or null when valid. Attach single-field rules to the control; attach cross-field rules (like "passwords match") to the parent FormGroup.

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

// Single field: reject values that are only whitespace.
export function notBlank(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = (control.value ?? '').toString().trim();
    return value.length === 0 ? { notBlank: true } : null;
  };
}

// Cross-field: applied to the FormGroup, not a single control.
export function passwordsMatch(): ValidatorFn {
  return (group: AbstractControl): ValidationErrors | null => {
    const password = group.get('password')?.value;
    const confirm = group.get('confirm')?.value;
    return password === confirm ? null : { passwordMismatch: true };
  };
}
// Cross-field validators go in the group's options, not on a control.
form = this.fb.nonNullable.group(
  {
    password: ['', [Validators.required, Validators.minLength(8)]],
    confirm: ['', Validators.required],
  },
  { validators: passwordsMatch() },
);

How do you validate a value against the server?

Async validators return a Promise or Observable of ValidationErrors | null — perfect for "is this username taken?" checks. Set updateOn: 'blur' so you don't fire a request on every keystroke, and watch the control's pending flag to show a spinner while it runs.

import { inject } from '@angular/core';
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { UserService } from './user.service';

export function usernameAvailable(): AsyncValidatorFn {
  const users = inject(UserService);
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    if (!control.value) {
      return of(null);
    }
    return users.isTaken(control.value).pipe(
      map((taken) => (taken ? { usernameTaken: true } : null)),
      catchError(() => of(null)),
    );
  };
}
// Wire built-in + async validators and choose when validation runs.
username: ['', {
  validators: [Validators.required, Validators.minLength(3)],
  asyncValidators: [usernameAvailable()],
  updateOn: 'blur',
}],

How do you show errors and submit the form?

Every control exposes state flags that decide when to show an error: valid/invalid, touched/untouched, dirty/pristine, and pending (while an async validator runs). A common pattern is "show the error only after the user has touched the field," using the new @if control flow.

On submit, call markAllAsTouched() so every error becomes visible at once, then read the typed getRawValue() (or form.value). Use patchValue for partial updates, setValue when you supply every control, and reset() to return to the initial values.

// Update programmatically
this.form.patchValue({ firstName: 'Ada' });   // partial update, OK
// this.form.setValue({ ... });               // must include every control
this.form.reset();                            // back to initial (non-null) values

onSubmit(): void {
  if (this.form.invalid) {
    this.form.markAllAsTouched();
    return;
  }
  this.api.save(this.form.getRawValue());
}

What about signal-based forms?

Angular's reactivity is moving to signals, and the team is designing a signal-based forms API so form state composes cleanly with the rest of a signal-driven app. It is experimental and not a replacement yet — both FormsModule (template-driven) and ReactiveFormsModule (reactive) remain fully supported, so the patterns above are safe to build on today.

New to the framework? Start with getting started with Angular and building client-side apps with Angular. Once your forms collect data, you'll usually protect the routes around them — see Angular authentication with route guards.

Need help shipping a complex, validated Angular app? Our Angular development services team has delivered 50+ projects since 2014.

Frequently Asked Questions

Should I use reactive or template-driven forms?

Use reactive forms for anything non-trivial — dynamic fields, cross-field rules, async validation, or code you want to unit test. Template-driven forms are fine for small, static forms (a login box or a single search field) where the simplicity of [(ngModel)] outweighs the extra structure.

What are typed reactive forms?

Since Angular 14, FormGroup, FormControl, and FormArray are generic and strongly typed. this.fb.group({ email: [''] }) infers a FormControl<string | null>, so the compiler catches wrong field names and value types. Use fb.nonNullable.group(...) to drop the null and have reset() restore the initial value.

Do I still need NgModule for Angular forms?

No. Standalone components are the default since Angular 17. Import ReactiveFormsModule or FormsModule directly in the component's imports array and bootstrap with bootstrapApplication in main.ts — no AppModule required.

How do I create a custom validator?

Write a ValidatorFn: a function that receives an AbstractControl and returns a ValidationErrors object (for example { notBlank: true }) when invalid, or null when valid. Attach single-field validators to the control and cross-field validators (like "passwords match") to the parent FormGroup.

Why aren't my validation errors showing up?

Most often the field hasn't been touched yet, so a control.touched guard hides the message. Inspect control.errors for the actual keys, confirm the validator is wired to the right place (control vs FormGroup), and call form.markAllAsTouched() on submit so every error appears at once.

Can I build dynamic, repeating form fields?

Yes — use FormArray with reactive forms. Push or remove FormControl/FormGroup instances at runtime (for example a list of phone numbers or invoice line items) and render them with @for. This is far cleaner than managing repeating fields in a template-driven form.

Share this article