Angular supports two form approaches: template-driven (simple, HTML-centric) and reactive (programmatic, scalable).

Template-Driven Forms

Import FormsModule and use ngModel:

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

@Component({
  selector: 'app-contact-form',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #form="ngForm" (ngSubmit)="onSubmit(form)">
      <input name="name" ngModel required #name="ngModel" placeholder="Name">
      @if (name.invalid && name.touched) {
        <span class="error">Name is required</span>
      }

      <input name="email" ngModel required email #email="ngModel" placeholder="Email">
      @if (email.invalid && email.touched) {
        <span class="error">Valid email required</span>
      }

      <button type="submit" [disabled]="form.invalid">Send</button>
    </form>
  `
})
export class ContactFormComponent {
  onSubmit(form: { value: { name: string; email: string } }) {
    console.log(form.value);
  }
}
  

Template-driven forms are quick to prototype but harder to test and scale.

Reactive Forms

Import ReactiveFormsModule and build a form in TypeScript:

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

@Component({
  selector: 'app-signup-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <input formControlName="username" placeholder="Username">
      @if (form.get('username')?.invalid && form.get('username')?.touched) {
        <span class="error">Username required (min 3 chars)</span>
      }

      <input formControlName="email" type="email" placeholder="Email">
      <input formControlName="password" type="password" placeholder="Password">

      <button type="submit" [disabled]="form.invalid">Sign Up</button>
    </form>
  `
})
export class SignupFormComponent {
  private fb = inject(FormBuilder);

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

  onSubmit() {
    if (this.form.valid) {
      console.log(this.form.value);
    }
  }
}
  

Form Groups and Nested Fields

  form = this.fb.group({
  profile: this.fb.group({
    firstName: ['', Validators.required],
    lastName: ['', Validators.required]
  }),
  address: this.fb.group({
    city: [''],
    zip: ['', Validators.pattern(/^\d{5}$/)]
  })
});
  

Template:

  <div formGroupName="profile">
  <input formControlName="firstName">
  <input formControlName="lastName">
</div>
  

FormArray — Dynamic Fields

  import { FormArray } from '@angular/forms';

skills = this.fb.array([
  this.fb.control('JavaScript', Validators.required)
]);

addSkill() {
  this.skills.push(this.fb.control('', Validators.required));
}

get skills(): FormArray {
  return this.form.get('skills') as FormArray;
}
  
  <div formArrayName="skills">
  @for (skill of skills.controls; track $index) {
    <input [formControlName]="$index" placeholder="Skill">
  }
  <button type="button" (click)="addSkill()">Add skill</button>
</div>
  

When to Use Which

Approach Best for
Template-driven Simple forms, quick prototypes
Reactive Complex validation, dynamic fields, unit tests

Reactive forms give you full control in TypeScript — prefer them for production applications with non-trivial validation.