Back to all posts

Forms in Angular: Reactive Forms


Reactive Forms in Angular offer a model-driven approach to handling form input and validation. Unlike Template-driven forms, Reactive Forms provide more control, better testing capabilities and these are more suitable for complex form scenarios.

Setting Up the Form

//signup.component.ts
import { ReactiveFormsModule } from '@angular/forms';

@Component({
  // ...
  imports: [ReactiveFormsModule]
})

Form Structure and Implementation

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

function equalValues(controlName1: string, controlName2: string) {
  return (control: AbstractControl) => {
    const val1 = control.get(controlName1)?.value
    const val2 = control.get(controlName2)?.value
    
    if(val1 === val2){
      return null
    }

    return {valueNotEqual: true}
  }
}


@Component({
  selector: 'app-signup',
  standalone: true,
  templateUrl: './signup.component.html',
  styleUrl: './signup.component.css',
  imports: [ReactiveFormsModule]
})


export class SignupComponent {

  //Create new form instance
  form = new FormGroup({
    email: new FormControl("",{
      validators: [Validators.email, Validators.required]
    }),
    passwords: new FormGroup({
      password: new FormControl("", {
        validators: [Validators.required, Validators.minLength(6)]
      }),
      confirmPassword: new FormControl("", {
        validators: [Validators.required, Validators.minLength(6)]
      }),
    }, {
      validators: [
        equalValues('password', 'confirmPassword')
      ]
    }),
    firstName: new FormControl("", {validators: [Validators.required]}),
    lastName: new FormControl("", {validators: [Validators.required]}),
    address: new FormGroup({
      street: new FormControl("", {validators: [Validators.required]}),
      number: new FormControl("", {validators: [Validators.required]}),
      postalCode: new FormControl("", {validators: [Validators.required]}),
      city: new FormControl("", {validators: [Validators.required]}),
    }),
    role: new FormControl<
      'student' | 'teacher' | 'employee' | 'founder' | 'other'
    >('student', {validators: [Validators.required]}),

    source: new FormArray([
      new FormControl(false),
      new FormControl(false),
      new FormControl(false),

    ]),
    agree: new FormControl(false, {validators: [Validators.required]})

  })

  onSubmit() {

    if(this.form.invalid){
      console.log('INVALID FORM')
      return;
    }
    console.log(this.form)
  }

  onReset() {
    this.form.reset()
  }
}
<form [formGroup]="form" (ngSubmit)="onSubmit()">
  <h2>Welcome on board!</h2>
  <p>We just need a little bit of data from you to get you started 🚀</p>

  <div class="control">
    <label for="email">Email</label>
    <input id="email" type="email" name="email" formControlName = "email"  />
  </div>

  <div class="control-row" formGroupName="passwords">
    <div class="control">
      <label for="password">Password</label>
      <input
        id="password"
        type="password"
        name="password"
        formControlName = "password" 
      />
    </div>

    <div class="control">
      <label for="confirm-password">Confirm Password</label>
      <input
        id="confirm-password"
        type="password"
        name="confirm-password"
        formControlName="confirmPassword"  
      />
    </div>
  </div>

  <hr />

  <div>
    <div class="control-row">
      <div class="control">
        <label for="first-name">First Name</label>
        <input
          type="text"
          id="first-name"
          name="first-name"
        formControlName="firstName"  

        />
      </div>

      <div class="control">
        <label for="last-name">Last Name</label>
        <input
          type="text"
          id="last-name"
          name="last-name"
          formControlName="lastName"
        />
      </div>
    </div>

    <fieldset formGroupName="address">
      <legend>Your Address</legend>

      <div class="control-row">
        <div class="control">
          <label for="street">Street</label>
          <input type="text" id="street" name="street"
          formControlName="street"
          />
        </div>

        <div class="control">
          <label for="number">Number</label>
          <input type="text" id="number" name="number" 
          formControlName="number"
          
          />
        </div>
      </div>

      <div class="control-row">
        <div class="control">
          <label for="postal-code">Postal Code</label>
          <input type="text" id="postal-code" name="postal-code" 
          formControlName="postalCode"
          
          />
        </div>

        <div class="control">
          <label for="city">City</label>
          <input type="text" id="city" name="city"
          formControlName="city"
          
          />
        </div>
      </div>
    </fieldset>
  </div>

  <hr />

  <div class="control-row">
    <div class="control">
      <label for="role">What best describes your role?</label>
      <select id="role" name="role" formControlName="role">
        <option value="student">Student</option>
        <option value="teacher">Teacher</option>
        <option value="employee">Employee</option>
        <option value="founder">Founder</option>
        <option value="other">Other</option>
      </select>
    </div>
  </div>

  <fieldset formArrayName="source">
    <legend>How did you find us?</legend>
    <div class="control">
      <input
        type="checkbox"
        id="google"
        name="acquisition"
        value="google"
        formControlName="0"
      />
      <label for="google">Google</label>
    </div>

    <div class="control">
      <input
        type="checkbox"
        id="friend"
        name="acquisition"
        value="friend"
        formControlName="1"
      />
      <label for="friend">Referred by friend</label>
    </div>

    <div class="control">
      <input
        type="checkbox"
        id="other"
        name="acquisition"
        value="other"
        formControlName="2"

      />
      <label for="other">Other</label>
    </div>
  </fieldset>

  <div class="control-row">
    <div class="control">
      <label for="terms-and-conditions">
        <input
          type="checkbox"
          id="terms-and-conditions"
          name="terms"
          formControlName="agree"
        />
        I agree to the terms and conditions
      </label>
    </div>
  </div>

  <p class="form-actions">
    <button type="reset" class="button button-flat" (click)="onReset()">Reset</button>
    <button type="submit" class="button">Sign up</button>
  </p>

  @if(form.touched && form.invalid) {
    <p class="control-error">
      Invalid form - please check yout input data.
    </p>
  }
</form>

1. Creating the Form Group

form = new FormGroup({
  email: new FormControl("", {
    validators: [Validators.email, Validators.required]
  }),
  // ... other form controls
});

The FormGroup is the foundation of our form, containing all form controls and nested groups.

2. Nested Form Groups

Our form uses nested groups for better organization:

passwords: new FormGroup({
  password: new FormControl(""),
  confirmPassword: new FormControl("")
}, {
  validators: [equalValues('password', 'confirmPassword')]
}),
address: new FormGroup({
  street: new FormControl(""),
  number: new FormControl(""),
  postalCode: new FormControl(""),
  city: new FormControl("")
})

3. Custom Validation

We’ve implemented a custom validator for password matching:

function equalValues(controlName1: string, controlName2: string) {
  return (control: AbstractControl) => {
    const val1 = control.get(controlName1)?.value
    const val2 = control.get(controlName2)?.value
    
    return val1 === val2 ? null : {valueNotEqual: true}
  }
}

4. Form Arrays

For multiple checkbox selections, we use FormArray:

source: new FormArray([
  new FormControl(false),
  new FormControl(false),
  new FormControl(false)
])

5. Type Safety

The form demonstrates type safety using TypeScript:

role: new FormControl
  'student' | 'teacher' | 'employee' | 'founder' | 'other'
>('student')

Template Integration

The template binds to our reactive form using several key directives:

<form [formGroup]="form" (ngSubmit)="onSubmit()">
  <input formControlName="email" type="email">
  <!-- Nested form group -->
  <div formGroupName="address">
    <input formControlName="street">
    <!-- ... -->
  </div>
  <!-- Form array -->
  <div formArrayName="source">
    <!-- ... -->
  </div>
</form>

Form Handling

1. Form Submission

onSubmit() {
  if(this.form.invalid){
    console.log('INVALID FORM')
    return;
  }
  console.log(this.form)
}

Form Reset

onReset() {
  this.form.reset()
}

Form Validation Features

  1. Built-in Validators:
    • Required fields: Validators.required
    • Email validation: Validators.email
    • Minimum length: Validators.minLength(6)
  2. Custom Validation:
    • Password matching
    • Group-level validation
  3. Validation Feedback:
@if(form.touched && form.invalid) {
  <p class="control-error">
    Invalid form - please check your input data.
  </p>
}