Angular Forms

Forms are one of the most common and important features of web applications. Angular provides a powerful and flexible forms system for collecting user input, validating that input, and responding to it. Angular supports two distinct approaches to building forms, each suited to different use cases.

Two Approaches to Angular Forms

Template-Driven Forms

Template-driven forms are built primarily in the HTML template using Angular directives. The form structure, validation, and data binding are all defined in the template. The TypeScript class holds very little form-related code. This approach is simpler and suitable for straightforward forms.

Reactive Forms

Reactive forms are built primarily in the TypeScript class. The form structure and validation rules are defined in code, and the template simply connects to that code. This approach is more powerful, more testable, and better suited for complex or dynamic forms.

Template-Driven Forms

Setup

Template-driven forms require FormsModule to be imported in the application module:


// app.module.ts
import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [BrowserModule, FormsModule]
})
export class AppModule { }

Basic Template-Driven Form


// registration.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-registration',
  templateUrl: './registration.component.html'
})
export class RegistrationComponent {

  formData = {
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    agreeToTerms: false
  };

  onSubmit() {
    console.log('Form submitted:', this.formData);
    alert(`Welcome, ${this.formData.firstName}!`);
  }
}

<!-- registration.component.html -->
<h3>Create Account</h3>

<form #registrationForm="ngForm" (ngSubmit)="onSubmit()">

  <div>
    <label>First Name</label>
    <input
      type="text"
      name="firstName"
      [(ngModel)]="formData.firstName"
      required
      minlength="2"
      #firstNameField="ngModel">
    <span *ngIf="firstNameField.invalid && firstNameField.touched">
      First name is required (minimum 2 characters).
    </span>
  </div>

  <div>
    <label>Email</label>
    <input
      type="email"
      name="email"
      [(ngModel)]="formData.email"
      required
      email
      #emailField="ngModel">
    <span *ngIf="emailField.invalid && emailField.touched">
      A valid email address is required.
    </span>
  </div>

  <div>
    <label>Password</label>
    <input
      type="password"
      name="password"
      [(ngModel)]="formData.password"
      required
      minlength="8"
      #passwordField="ngModel">
    <span *ngIf="passwordField.invalid && passwordField.touched">
      Password must be at least 8 characters.
    </span>
  </div>

  <div>
    <label>
      <input type="checkbox" name="agreeToTerms" [(ngModel)]="formData.agreeToTerms" required>
      I agree to the Terms and Conditions
    </label>
  </div>

  <button type="submit" [disabled]="registrationForm.invalid">Create Account</button>

</form>

Key Points in Template-Driven Forms

  • #registrationForm="ngForm" — Creates a template reference to the entire form object, exposing properties like invalid, valid, touched, and dirty.
  • name attribute — Required on every form field when using ngModel inside a form.
  • [(ngModel)] — Binds the field to a property in the TypeScript class.
  • #emailField="ngModel" — Creates a reference to the individual field's state, used to show/hide validation messages.
  • HTML5 validation attributes like required, minlength, and email work alongside Angular's validation.
  • touched — True after the user has focused on and left the field. Showing errors only when touched prevents validation messages from appearing before the user has interacted.

Reactive Forms

Setup

Reactive forms require ReactiveFormsModule to be imported in the application module:


// app.module.ts
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  imports: [BrowserModule, ReactiveFormsModule]
})
export class AppModule { }

Building a Reactive Form

Reactive forms use FormGroup, FormControl, and FormBuilder from @angular/forms. The entire form structure is defined in the TypeScript class.


// contact.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-contact',
  templateUrl: './contact.component.html'
})
export class ContactComponent implements OnInit {

  contactForm!: FormGroup;
  submitted = false;

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.contactForm = this.fb.group({
      name: ['', [Validators.required, Validators.minLength(3)]],
      email: ['', [Validators.required, Validators.email]],
      subject: ['', Validators.required],
      message: ['', [Validators.required, Validators.minLength(20)]]
    });
  }

  // Shortcut to access form controls
  get f() {
    return this.contactForm.controls;
  }

  onSubmit() {
    this.submitted = true;
    if (this.contactForm.valid) {
      console.log('Form Data:', this.contactForm.value);
      alert('Message sent successfully!');
      this.contactForm.reset();
      this.submitted = false;
    }
  }
}

<!-- contact.component.html -->
<h3>Contact Us</h3>

<form [formGroup]="contactForm" (ngSubmit)="onSubmit()">

  <div>
    <label>Name</label>
    <input type="text" formControlName="name">
    <div *ngIf="f['name'].invalid && (f['name'].touched || submitted)">
      <small *ngIf="f['name'].errors?.['required']">Name is required.</small>
      <small *ngIf="f['name'].errors?.['minlength']">Minimum 3 characters.</small>
    </div>
  </div>

  <div>
    <label>Email</label>
    <input type="email" formControlName="email">
    <div *ngIf="f['email'].invalid && (f['email'].touched || submitted)">
      <small *ngIf="f['email'].errors?.['required']">Email is required.</small>
      <small *ngIf="f['email'].errors?.['email']">Enter a valid email.</small>
    </div>
  </div>

  <div>
    <label>Message</label>
    <textarea formControlName="message" rows="5"></textarea>
    <div *ngIf="f['message'].invalid && (f['message'].touched || submitted)">
      <small *ngIf="f['message'].errors?.['required']">Message is required.</small>
      <small *ngIf="f['message'].errors?.['minlength']">Minimum 20 characters.</small>
    </div>
  </div>

  <button type="submit">Send Message</button>

</form>

Custom Validators in Reactive Forms

Angular allows creating custom validators for business-specific validation rules. A validator is a function that receives a FormControl and returns either null (valid) or an error object (invalid).


// validators/no-spaces.validator.ts
import { AbstractControl, ValidationErrors } from '@angular/forms';

export function noSpacesValidator(control: AbstractControl): ValidationErrors | null {
  const value = control.value as string;
  if (value && value.includes(' ')) {
    return { noSpaces: true };  // Return error object
  }
  return null;  // No error — the field is valid
}

// Using the custom validator
this.loginForm = this.fb.group({
  username: ['', [Validators.required, noSpacesValidator]],
  password: ['', [Validators.required, Validators.minLength(8)]]
});

<!-- Displaying the custom error message -->
<small *ngIf="f['username'].errors?.['noSpaces']">
  Username cannot contain spaces.
</small>

FormArray — Dynamic Form Fields

FormArray allows managing a list of form controls dynamically. This is useful when the user can add or remove fields — such as adding multiple phone numbers or skills.


// skills-form.component.ts
import { FormBuilder, FormArray, Validators } from '@angular/forms';

export class SkillsFormComponent {
  skillsForm = this.fb.group({
    name: ['', Validators.required],
    skills: this.fb.array([this.fb.control('')])  // Start with one skill field
  });

  constructor(private fb: FormBuilder) {}

  get skills() {
    return this.skillsForm.get('skills') as FormArray;
  }

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

  removeSkill(index: number) {
    this.skills.removeAt(index);
  }
}

<!-- skills-form.component.html -->
<form [formGroup]="skillsForm">
  <input formControlName="name" placeholder="Your name">

  <h4>Skills</h4>
  <div formArrayName="skills">
    <div *ngFor="let skill of skills.controls; let i = index">
      <input [formControlName]="i" placeholder="Enter a skill">
      <button type="button" (click)="removeSkill(i)">Remove</button>
    </div>
  </div>

  <button type="button" (click)="addSkill()">+ Add Skill</button>
  <button type="submit">Save Profile</button>
</form>

Template-Driven vs Reactive Forms Comparison


Feature                | Template-Driven        | Reactive
-----------------------|------------------------|----------------------------
Form definition        | HTML template          | TypeScript class
Code complexity        | Low                    | Higher
Dynamic fields         | Complex                | Easy (FormArray)
Unit testing           | Harder                 | Easier
Suitable for           | Simple forms           | Complex, dynamic forms
Validation             | HTML attributes        | Validators functions
Real-time validation   | Possible               | Straightforward

Summary

Angular provides two approaches to building forms: template-driven and reactive. Template-driven forms use FormsModule, ngModel, and HTML validation attributes — they are simpler and suitable for basic forms. Reactive forms use ReactiveFormsModule, FormGroup, FormControl, and FormBuilder — they offer greater control, are more testable, and handle complex scenarios better. Both approaches provide built-in validation through Angular's validator system. Custom validators are functions that return null for valid controls or an error object for invalid ones. FormArray manages dynamic lists of form fields that users can add or remove at runtime.

Leave a Comment

Your email address will not be published. Required fields are marked *