Angular Forms

Angular ships two complementary approaches to building forms — template-driven forms for quick, simple cases and reactive forms for complex, testable, dynamically validated ones.

Two Ways to Build Forms

Template-driven forms let Angular infer the form model from directives placed directly in your HTML, driven by ngModel and the FormsModule. Reactive forms flip that around: you define the form model explicitly in TypeScript as a tree of FormControl and FormGroup instances, then bind your template to that model. Both styles produce the same runtime forms API underneath, so the choice comes down to how much control and testability you need.

Template-Driven Forms

To use template-driven forms, import FormsModule into your standalone component and attach ngModel to each input you want tracked. Angular automatically creates a NgForm instance on the <form> element (accessible via a template reference variable like #signupForm="ngForm") and a NgModel instance on each control, complete with validity state you can inspect directly in the template.

A template-driven signup form

import { Component } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
import { bootstrapApplication } from '@angular/platform-browser';

@Component({
  selector: 'app-signup-form',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #signupForm="ngForm" (ngSubmit)="onSubmit(signupForm)">
      <label>
        Email
        <input
          name="email"
          type="email"
          required
          email
          ngModel
          #emailField="ngModel"
        />
      </label>
      @if (emailField.invalid && emailField.touched) {
        <p class="error">Please enter a valid email address.</p>
      }

      <label>
        Password
        <input name="password" type="password" required minlength="8" ngModel />
      </label>

      <button type="submit" [disabled]="signupForm.invalid">Sign up</button>
    </form>
  `
})
export class SignupFormComponent {
  onSubmit(form: NgForm): void {
    if (form.valid) {
      console.log('Submitted value:', form.value);
    }
  }
}

bootstrapApplication(SignupFormComponent).catch((err) => console.error(err));

Reactive Forms

Reactive forms move the source of truth into TypeScript. You build the form with FormBuilder (or by hand with `new FormGroup(...)`), attach validators as plain functions, and bind the template with formGroup and formControlName. Because the model exists independently of the DOM, it's straightforward to unit test, patch values programmatically, or react to value changes with observables like valueChanges.

A reactive profile form

import { Component } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { bootstrapApplication } from '@angular/platform-browser';

@Component({
  selector: 'app-profile-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
      <label>
        Username
        <input formControlName="username" />
      </label>
      @if (profileForm.get('username')?.invalid && profileForm.get('username')?.touched) {
        <p class="error">Username must be at least 3 characters.</p>
      }

      <label>
        Bio
        <textarea formControlName="bio"></textarea>
      </label>

      <button type="submit" [disabled]="profileForm.invalid">Save profile</button>
    </form>
  `
})
export class ProfileFormComponent {
  profileForm = this.fb.group({
    username: ['', [Validators.required, Validators.minLength(3)]],
    bio: ['']
  });

  constructor(private fb: FormBuilder) {}

  onSubmit(): void {
    if (this.profileForm.valid) {
      console.log('Profile saved:', this.profileForm.value);
    }
  }
}

bootstrapApplication(ProfileFormComponent).catch((err) => console.error(err));
  • The form model lives in TypeScript, so it's testable without rendering a template
  • Custom validators are plain functions, easy to unit test in isolation
  • You can subscribe to valueChanges and statusChanges as observables
  • Dynamic forms (adding or removing controls at runtime) are far simpler
  • No two-way binding lag between keystrokes and the model
AspectTemplate-drivenReactive
Module to importFormsModuleReactiveFormsModule
Form model sourceInferred from the DOM via ngModelDefined explicitly in TypeScript
ValidatorsHTML attributes (required, minlength)Validator functions passed in code
Best forSmall, simple formsComplex, dynamic, or heavily tested forms

A custom validator with error messages

import { Component } from '@angular/core';
import {
  ReactiveFormsModule,
  FormBuilder,
  Validators,
  AbstractControl,
  ValidationErrors
} from '@angular/forms';
import { bootstrapApplication } from '@angular/platform-browser';

function noSpacesValidator(control: AbstractControl): ValidationErrors | null {
  const hasSpaces = /\s/.test(control.value ?? '');
  return hasSpaces ? { noSpaces: true } : null;
}

@Component({
  selector: 'app-account-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="accountForm" (ngSubmit)="onSubmit()">
      <label>
        Account handle
        <input formControlName="handle" />
      </label>

      @if (accountForm.get('handle')?.hasError('required')) {
        <p class="error">A handle is required.</p>
      }
      @if (accountForm.get('handle')?.hasError('noSpaces')) {
        <p class="error">Handles cannot contain spaces.</p>
      }

      <button type="submit" [disabled]="accountForm.invalid">Create account</button>
    </form>
  `
})
export class AccountFormComponent {
  accountForm = this.fb.group({
    handle: ['', [Validators.required, noSpacesValidator]]
  });

  constructor(private fb: FormBuilder) {}

  onSubmit(): void {
    console.log(this.accountForm.value);
  }
}

bootstrapApplication(AccountFormComponent).catch((err) => console.error(err));
Note: Reach for reactive forms by default on anything beyond a two-field contact form. The upfront cost of defining the model in TypeScript pays off quickly once you need conditional validators, dynamically added fields, or thorough unit tests.
Note: Standalone components don't get FormsModule or ReactiveFormsModule for free — forgetting to add the right one to the component's `imports` array is the most common reason ngModel or formControlName silently fails to bind.

Exercise: Angular Forms

What are the two main approaches to building Angular forms?