Tutorials Logic, IN info@tutorialslogic.com

Angular Forms Reactive Template Driven

Angular Form Models

Angular provides Signal Forms, reactive forms, and template-driven forms. Compare their state models, validation styles, and testing tradeoffs, then choose the approach that fits a new signal-based screen or an established application.

Angular offers three form models. Reactive forms define an explicit control tree in TypeScript. Template-driven forms let directives build that tree from template markup. Signal Forms build a typed field tree around a writable signal model.

Choose by ownership and complexity. Reactive forms fit established applications, dynamic control trees, and observable workflows. Template-driven forms suit small forms with simple validation. Signal Forms fit new signal-first features that benefit from typed field state and schema validation.

Model Source of truth Good fit
Reactive FormControl and FormGroup in TypeScript Complex or dynamic forms and existing reactive-form codebases
Template-driven Directives such as ngModel in HTML Small forms with straightforward fields
Signal Forms Writable model signal plus field schema Typed, signal-first forms

Reactive Forms

Reactive forms make the complete form model available in TypeScript. Import ReactiveFormsModule in the standalone component, create non-nullable controls when null is not a valid domain value, and submit with getRawValue after checking validity.

Use formControlName inside a parent formGroup. Control status includes valid, invalid, pending, disabled, pristine, dirty, touched, and untouched; use those states to decide when feedback should appear.

Reactive Forms CLI Command

Reactive Forms CLI Command
ng generate component reactive-form

Reactive Form - Standalone

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

@Component({
  selector: 'app-profile-form',
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="profile" (ngSubmit)="save()">
      <label for="email">Email</label>
      <input id="email" type="email" formControlName="email" />
      @if (email.invalid && email.touched) {
        <p role="alert">Enter a valid email address.</p>
      }
      <button type="submit">Save</button>
    </form>
  `
})
export class ProfileFormComponent {
  private fb = inject(FormBuilder).nonNullable;
  profile = this.fb.group({
    email: ['', [Validators.required, Validators.email]]
  });
  get email() { return this.profile.controls.email; }

  save(): void {
    if (this.profile.invalid) {
      this.profile.markAllAsTouched();
      return;
    }
    console.log(this.profile.getRawValue());
  }
}

The nonNullable builder keeps email typed as string. The submit handler marks hidden errors as touched, exits on invalid input, and only then reads the form value.

Dynamic Controls with FormArray

Use FormArray when the number of controls is data-driven, such as order lines, phone numbers, or survey answers. The array owns indexed controls; each row can be a FormControl or a typed FormGroup.

Use a stable domain identifier to track rendered rows when available. Removing a row changes control indexes, so do not use the current index as a persistent backend identity.

  • Create every row through one factory so validators and non-nullable types stay consistent.
  • Use clear, removeAt, insert, and push rather than mutating controls directly.
  • Validate relationships such as at least one row on the FormArray itself.

Typed Order Lines

Typed Order Lines
private createLine() {
  return this.fb.group({
    productId: ['', Validators.required],
    quantity: [1, [Validators.required, Validators.min(1)]]
  });
}

readonly order = this.fb.group({
  customerId: ['', Validators.required],
  lines: this.fb.array([this.createLine()])
});

get lines() {
  return this.order.controls.lines;
}

addLine(): void {
  this.lines.push(this.createLine());
}

removeLine(index: number): void {
  this.lines.removeAt(index);
}

Submit Reset and Server Data

On submit, reject invalid or pending state, mark controls for feedback, map the form value to the API command, and prevent duplicate submissions while the request is active. The backend must validate the same business and security rules.

Use setValue when the complete shape is required and patchValue for a deliberate partial update. reset changes value and interaction state; pass an explicit value when non-nullable defaults or persisted identifiers must be restored.

  • Do not disable submit permanently after a failed request; release it in finalize.
  • Map server field errors back to matching controls and keep form-level errors near the submit action.
  • Unsaved-change checks should compare meaningful domain values, not only the dirty flag.

Submit Once and Restore the Form

Submit Once and Restore the Form
save(): void {
  if (this.order.invalid || this.order.pending || this.saving()) {
    this.order.markAllAsTouched();
    return;
  }

  this.saving.set(true);
  const command = this.order.getRawValue();

  this.orders.create(command).pipe(
    finalize(() => this.saving.set(false))
  ).subscribe({
    next: saved => {
      this.lines.clear();
      this.lines.push(this.createLine());
      this.order.reset({ customerId: saved.customerId });
    },
    error: error => this.applyServerErrors(error)
  });
}

Template-driven Forms

Template-driven forms use FormsModule, ngForm, ngModel, and HTML-like validation attributes. Every control inside a form needs a name so Angular can register it. Export ngModel to inspect one field without repeatedly looking it up through the form.

This style keeps small forms concise, but business logic spread across template directives becomes harder to test and refactor as conditional fields and cross-field rules grow.

Template-driven Forms CLI Command

Template-driven Forms CLI Command
ng generate component template-form

Template-driven Form - Standalone

Template-driven Form - Standalone
import { Component } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';

@Component({
  selector: 'app-contact-form',
  imports: [FormsModule],
  template: `
    <form #contact="ngForm" (ngSubmit)="send(contact)">
      <label for="email">Email</label>
      <input id="email" name="email" type="email"
             [(ngModel)]="model.email" #email="ngModel" required email />
      @if (email.invalid && email.touched) {
        <p role="alert">Enter a valid email address.</p>
      }
      <button type="submit">Send</button>
    </form>
  `
})
export class ContactFormComponent {
  model = { email: '' };
  send(form: NgForm): void {
    if (form.invalid) return;
    console.log(this.model);
  }
}

name registers the control, ngModel creates two-way binding, and #email="ngModel" exposes field state for focused feedback.

Signal Forms

Signal Forms are stable in Angular 22. They build a typed field tree from a writable signal model, bind controls with FormField, and define validation through a schema. They are a strong fit for new signal-based applications; reactive forms remain a reliable choice for established codebases and complex dynamic form workflows.

  • Form data lives in a plain signal that you control.
  • Angular derives the form structure from the data shape.
  • Validation rules are defined as a schema in code.
  • Uses the form(), schema(), and Field directive from @angular/forms/signals.

Signal Login Form

Signal Login Form
import { Component, signal } from '@angular/core';
import { Field, form, required, email } from '@angular/forms/signals';

@Component({
  selector: 'app-login',
  imports: [Field],
  template: `
    <label for="email">Email</label>
    <input id="email" type="email" [field]="loginForm.email" />
    @for (error of loginForm.email().errors(); track error.kind) {
      <p role="alert">{{ error.message }}</p>
    }
  `
})
export class LoginComponent {
  model = signal({ email: '' });
  loginForm = form(this.model, path => {
    required(path.email, { message: 'Email is required' });
    email(path.email, { message: 'Enter a valid email' });
  });
}

The writable signal owns submitted data, form creates a typed field tree, and Field synchronizes the input with field state and validation.

Confirm the page outcome

Form Model Review

4 checks
  • I can choose reactive, template-driven, or Signal Forms from the form's complexity and state model.
  • I can build a copyable form, expose accessible validation feedback, block invalid submission, and read typed values.
  • I know that client validation improves feedback but the server must validate every submitted value again.
  • I can build dynamic FormArray rows, load server data, prevent duplicate submission, map server errors, and reset state deliberately.

Form Model Review Questions

Reactive and template-driven forms use different directive packages. A component using [formGroup] and formControlName must import ReactiveFormsModule; one using ngModel or ngForm must import FormsModule.

Most likely, the input has no name. NgForm uses that name as the key in its controls collection and submitted value.

Yes. Signal Forms are stable starting in Angular 22. Choose them for new signal-based forms when their field-tree and schema model fit the screen; keep reactive forms when migration cost or an established control-based architecture makes a rewrite unnecessary.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.