Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 9: Forms in Angular

Most applications need to collect user input — login pages, search boxes, registration forms, checkout screens. Angular offers two powerful ways to handle forms:

  1. Template-driven forms — simple, declarative, good for small forms.
  2. Reactive forms — explicit, programmatic, great for complex or dynamic forms.

Both approaches share the same goals:

  • Capture user input.
  • Track state (dirty, touched, valid).
  • Validate input.
  • React to changes.

In this chapter, we’ll explore both strategies in detail.


9.1 Template-Driven Forms

Template-driven forms use Angular’s directives like ngModel inside templates. Most of the logic lives in the HTML, and Angular builds the form model under the hood.

Example: Login Form (Template-Driven)

TypeScript (Component):

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

@Component({
  selector: 'app-login',
  standalone: true,
  templateUrl: './login.component.html'
})
export class LoginComponent {
  email: string = '';
  password: string = '';

  login() {
    console.log('Email:', this.email);
    console.log('Password:', this.password);
  }
}

Template:

<form (ngSubmit)="login()">
  <label>Email</label>
  <input type="email" [(ngModel)]="email" name="email" required>

  <label>Password</label>
  <input type="password" [(ngModel)]="password" name="password" required>

  <button type="submit">Login</button>
</form>

✅ Here:

  • [(ngModel)] binds input values to component properties.
  • name attribute is required for Angular to track each control.
  • Validation (required) is handled via HTML5 + Angular’s directives.

9.2 Validation in Template-Driven Forms

Angular automatically applies CSS classes to inputs:

  • ng-valid / ng-invalid
  • ng-touched / ng-untouched
  • ng-dirty / ng-pristine

Example:

<input type="email" [(ngModel)]="email" name="email" required>
<div *ngIf="emailControl.invalid && emailControl.touched">
  Email is required!
</div>

You can also use Angular’s #ref="ngModel" to access state:

<input type="email" [(ngModel)]="email" name="email" required #emailCtrl="ngModel">
<div *ngIf="emailCtrl.invalid && emailCtrl.touched">
  Invalid email!
</div>

9.3 Reactive Forms

Reactive forms are more programmatic. Instead of Angular inferring the form model, you explicitly create it using FormGroup, FormControl, and FormBuilder.

Example: Registration Form (Reactive)

TypeScript (Component):

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

@Component({
  selector: 'app-register',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './register.component.html'
})
export class RegisterComponent {
  registerForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.registerForm = this.fb.group({
      name: ['', Validators.required],
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(6)]]
    });
  }

  submit() {
    if (this.registerForm.valid) {
      console.log(this.registerForm.value);
    }
  }
}

Template:

<form [formGroup]="registerForm" (ngSubmit)="submit()">
  <label>Name</label>
  <input formControlName="name">
  <div *ngIf="registerForm.get('name')?.invalid && registerForm.get('name')?.touched">
    Name is required
  </div>

  <label>Email</label>
  <input formControlName="email">
  <div *ngIf="registerForm.get('email')?.invalid && registerForm.get('email')?.touched">
    Enter a valid email
  </div>

  <label>Password</label>
  <input type="password" formControlName="password">
  <div *ngIf="registerForm.get('password')?.invalid && registerForm.get('password')?.touched">
    Password must be at least 6 characters
  </div>

  <button type="submit" [disabled]="registerForm.invalid">Register</button>
</form>

9.4 Reactive vs Template-Driven

FeatureTemplate-DrivenReactive
Form model locationIn the template (ngModel)In the component class
Code styleDeclarative, less boilerplateProgrammatic, more explicit
ValidationBuilt-in directivesValidators in code
Best forSimple forms, quick prototypesComplex forms, dynamic logic

9.5 Advanced Reactive Features

9.5.1 FormArray

Dynamic arrays of controls. Example: adding multiple phone numbers.

phones: FormArray;

this.phones = this.fb.array([this.fb.control('')]);

9.5.2 Dynamic Form Controls

Create controls at runtime based on server config.

9.5.3 Async Validators

Check availability with backend (e.g., unique username).

Validators.composeAsync([this.userService.usernameTakenValidator()])

9.8 Forms with Signals

Signals make it easier to manage form state without constantly reaching into FormControl objects. Starting with Angular v17, you can bridge Reactive Forms and Signals using the new valueChanges signal utilities. This approach combines the predictability of signals with the structure of reactive forms.


9.8.1 Converting Form Controls to Signals

Angular provides a helper:

import { toSignal } from '@angular/core/rxjs-interop';

This converts an Observable (like FormControl.valueChanges) into a signal.

Example:

import { Component, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { toSignal } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-signal-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <input [formControl]="nameControl">
    <p>You typed: {{ name() }}</p>
  `
})
export class SignalFormComponent {
  nameControl = new FormControl('');
  name = toSignal(this.nameControl.valueChanges, { initialValue: '' });
}

Here:

  • toSignal() wraps the valueChanges Observable.
  • The template can read the value simply by calling name().

9.8.2 Direct Signal State Management

For very simple forms, you don’t even need FormControl. You can bind inputs directly to signals.

Example:

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

@Component({
  selector: 'app-signal-login',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form (ngSubmit)="submit()">
      <label>Email</label>
      <input [(ngModel)]="email()" (ngModelChange)="email.set($event)" name="email">

      <label>Password</label>
      <input type="password" [(ngModel)]="password()" (ngModelChange)="password.set($event)" name="password">

      <button type="submit">Login</button>
    </form>
    <p>Email preview: {{ email() }}</p>
  `
})
export class SignalLoginComponent {
  email = signal('');
  password = signal('');

  submit() {
    console.log('Email:', this.email());
    console.log('Password:', this.password());
  }
}

Here, signal is used directly as the form state — no FormControl or FormGroup needed. This works great for small forms.


9.8.3 Mixing Signals and Reactive Forms

You can combine both approaches: use Reactive Forms for validation and structure, but expose state as signals for easy reactivity.

@Component({
  selector: 'app-register-signal',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="submit()">
      <input formControlName="username" placeholder="Username">
      <p *ngIf="usernameError()">❌ Username is required</p>

      <button type="submit">Register</button>
    </form>
  `
})
export class RegisterSignalComponent {
  form = this.fb.group({
    username: ['', Validators.required]
  });

  username = toSignal(this.form.get('username')!.valueChanges, { initialValue: '' });
  usernameError = computed(() => this.form.get('username')!.invalid && this.form.get('username')!.touched);

  constructor(private fb: FormBuilder) {}

  submit() {
    console.log('Form value:', this.form.value);
  }
}

✅ Here we use:

  • toSignal() to track the username’s value as a signal.
  • computed() to derive validation state reactively.

9.8.4 When to Use Signal-Based Forms

  • ✅ Use direct signals for very simple forms (like a login or search input).
  • ✅ Use Reactive Forms + toSignal() for complex forms where validation, dynamic controls, or async rules are required.
  • 🚫 Avoid replacing all forms with signals today — Angular’s form APIs are still primarily built around FormControl and FormGroup, and signals complement them rather than replace them.

9.9 Chapter Summary

  • Angular supports three styles of forms:
    1. Template-driven (simple, declarative).
    2. Reactive (structured, programmatic).
    3. Signal-based (lightweight, reactive, new in Angular 17).
  • Signals integrate seamlessly with form controls using toSignal().
  • For small forms, signals can completely replace controls.
  • For complex forms, signals work best on top of Reactive Forms.