Quick answer: Angular has two forms systems. Template-driven forms use ngModel and validation attributes in the markup — quick for small forms. Reactive forms build a model in the class with FormControl, FormGroup and FormBuilder — better for complex, testable forms. Use template-driven for a login box, reactive for anything with dynamic fields, cross-field rules or deep validation.

Template-driven forms

Template-driven forms keep the logic in the template. You import FormsModule, wrap the fields in a <form>, and bind each input with [(ngModel)] and a name.

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

@Component({
  selector: 'app-login',
  imports: [FormsModule],
  template: `
    <form #f="ngForm" (ngSubmit)="login(f)">
      <input name="email" type="email" [(ngModel)]="email"
             required email #emailRef="ngModel" />
      @if (emailRef.invalid && emailRef.touched) {
        <p class="error">Enter a valid email.</p>
      }
      <button [disabled]="f.invalid">Sign in</button>
    </form>
  `,
})
export class LoginComponent {
  email = '';
  login(form: NgForm) { /* form.value has the fields */ }
}

The name attribute is required: it is how the form groups registers the control.

A template reference such as #emailRef="ngModel" exposes that control’s state — invalid, touched, dirty — so you can show an error only after the user has interacted.

Reactive forms

Reactive forms define the model in code, which makes it typed, testable and easy to shape dynamically. Import ReactiveFormsModule and build a group in the class.

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

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

  form = this.fb.group({
    name: ['', [Validators.required, Validators.minLength(2)]],
    email: ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, Validators.minLength(8)]],
  });

  submit() {
    if (this.form.invalid) {
      this.form.markAllAsTouched();
      return;
    }
    console.log(this.form.getRawValue());
  }
}

The template binds each field with formControlName and reads validation state from the control:

<form [formGroup]="form" (ngSubmit)="submit()">
  <input formControlName="email" type="email" />
  @if (form.controls.email.touched && form.controls.email.invalid) {
    <p class="error">Enter a valid email.</p>
  }
  <button [disabled]="form.invalid">Create account</button>
</form>

Which one should you choose?

AspectTemplate-drivenReactive
Model lives inThe templateThe class
SetupFormsModuleReactiveFormsModule
Best forShort, simple formsComplex, dynamic, testable forms
Dynamic fieldsAwkwardStraightforward with FormArray
TestingThrough the DOMDirectly on the model

The practical rule: if you can describe the form in a handful of fields and no conditional logic, template-driven is fine.

The moment you add a field list that grows, a rule that depends on another field, or a form you want to unit-test, switch to reactive.

Cross-field and async validation

Reactive forms make advanced validation straightforward. A custom validator is just a function that receives a control and returns an error object or null.

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

export function passwordMatch(group: AbstractControl): ValidationErrors | null {
  const pw = group.get('password')?.value;
  const confirm = group.get('confirm')?.value;
  return pw === confirm ? null : { mismatch: true };
}

Apply it to the whole group with { validators: passwordMatch }, which lets you compare two fields. An async validator does the same but returns a Promise or Observable — ideal for checking whether a username is already taken, with built-in debouncing.

Continue with the HTTP client and REST APIs. To learn Angular forms with real projects, Ampersand Academy teaches Angular one-to-one.

Frequently asked questions

Should I use template-driven or reactive forms in Angular?

Use template-driven forms for short, simple forms and reactive forms for complex ones. Reactive forms are typed, testable and handle dynamic fields and cross-field rules more easily.

Why does my ngModel input not register in the form?

Each ngModel input inside a form needs a name attribute. Without it, the form cannot register the control and validation and value access will not work.

How do I show validation errors only after the user types?

Check the control’s touched flag as well as invalid. Show the error when the control is invalid and touched, so it does not appear before the user interacts.

How do I validate that two fields match?

Write a cross-field validator that takes the whole group, reads both controls, and returns an error object when they differ. Apply it to the group rather than a single control.

What is an async validator in Angular?

A validator that returns a Promise or Observable, used when the check needs the server, such as confirming a username is free. Angular handles the pending state for you.

Last updated on · Written by