Angular Forms: Choosing Between Reactive and Template Driven Forms

Exploring the differencies between reactive and template driven forms.

ยท

10 min read

Play this article

When we start to build an application in Angular and need to create forms, we must pick one of the two flavors: "Reactive" or "Template Forms".

For beginners, Template Forms are natural and appear less complex for new joiners, but some developers may try to convince you that "If you want to have true control then you must use Reactive Forms".

Do I believe the most effective method for determining which option is superior is by solving the same problem with both alternatives?

Scenario

I'm working on a website for a Flight company that needs a form for users to search for flights

  • One Way: hide the return date picker.

  • All fields are required.

  • Disabled the search if some field is empty.

The form will look like this:

I don't focus too much on UI Styles; I mainly work on building the form and its behavior.

Before we create an identical form using both methods, let's first delve into the basics of Angular forms.

Before Start

At the core, both Reactive and template-driven forms share some common features and behaviors.

The Angular Forms module provides various built-in services, directives, and validators for managing forms.

Both use the FormGroup and FormControl classes to create and manage the form model and its data. The template-driven makes it easy to interact with these classes, and reactive-form prefers more code.

While reactive and template-driven forms differ in managing form data and behavior, they share a common set of features and tools provided by the Angular Forms module.

The Template-Driven Form

Let's create the template-driven form using the angular/cli.

ng g c components/flights

We must import the FormsModule in the app.module

Open flights.component.ts and declare the data for our form.

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

@Component({
  selector: 'app-flights',
  templateUrl: './flights.component.html',
  styleUrls: ['./flights.component.css']
})
export class FlightsComponent {

  flightType: string = "";
  from: string = "";
  to: string = "";
  depart: string = "";
  return: string = "";
  passengers: number = 1;
  passengerOptions: number[] = [1, 2, 3, 4, 5];

  onSubmit(flightForm: any) {
    console.log(flightForm.value);
  }
}

The form data can be accessed using two-way data binding syntax, which allows you to bind the form data to the component's properties and update.

First, declare the form #flightForm object using the ngForm and add the form controls for selecting the flight type, origin and destination airports, departure and return dates, and the number of passengers.

Bound to properties in the component using the ngModel directive, which enables two-way data binding. In the form template, we add the required attribute and the [disabled] property to enable or disable the submit button based on the form's validity.

<form #flightForm="ngForm" (ngSubmit)="onSubmit(flightForm)" novalidate>
  <div>
    <label>Flight:</label>
    <input
      type="radio"
      name="flightType"
      value="roundtrip"
      [(ngModel)]="flightType"
      required
    />Round trip
    <input
      type="radio"
      name="flightType"
      value="oneway"
      [(ngModel)]="flightType"
      required
    />One way
  </div>
  <div>
    <label>From:</label>
    <select name="from" [(ngModel)]="from" required>
      <option value="" disabled>Select an airport</option>
      <option value="JFK">John F. Kennedy International Airport</option>
      <option value="LAX">Los Angeles International Airport</option>
      <option value="ORD">O'Hare International Airport</option>
      <option value="DFW">Dallas/Fort Worth International Airport</option>
    </select>
  </div>
  <div>
    <label>To:</label>
    <select name="to" [(ngModel)]="to" required>
      <option value="" disabled>Select an airport</option>
      <option value="JFK">John F. Kennedy International Airport</option>
      <option value="LAX">Los Angeles International Airport</option>
      <option value="ORD">O'Hare International Airport</option>
      <option value="DFW">Dallas/Fort Worth International Airport</option>
    </select>
  </div>
  <div>
    <label>Depart:</label>
    <input type="date" name="depart" [(ngModel)]="depart" required />
  </div>
  <div *ngIf="flightType === 'roundtrip'">
    <label>Return:</label>
    <input type="date" name="return" [(ngModel)]="return" required />
  </div>
  <div>
    <label>Passengers:</label>
    <select name="passengers" [(ngModel)]="passengers" required>
      <option value="" disabled>Select number of passengers</option>
      <option *ngFor="let i of passengerOptions" [value]="i">{{ i }}</option>
    </select>
  </div>
  <button type="submit" [disabled]="!flightForm.valid">Submit</button>
</form>

Save the changes, and our form works.

The template-driven forms are user-friendly and use the native Html attributes like required, making it an effortless task for newcomers familiar with ngModel. Our next challenge is to construct it using reactive forms.

The Reactive Form

Again, create the component using angular/cli, but with another name.

ng g c components/flights-reactive

We must import the FormsModule and ReactiveFormsModule in the app.module

Open flights-reactive.component.ts, and start with the reactive form.

  • Declare the flightForm object of type FormGroup

  • Add the airports with their respective codes and names, the same with the passengerOptions.

  • Inject the Formbuilder in the constructor.

  • Add the form controls for flightType, from, to, depart, return, and passengers, and specifies their initial values and validation rules.

  • Set the flightType, from, to, depart, and passengers form controls are all marked as required using the Validators.required .

  • The return form control is not required, as it is only needed when the flight type is "roundtrip".

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

@Component({
  selector: 'app-flights-reactive',
  templateUrl: './flights-reactive.component.html',
  styleUrls: ['./flights-reactive.component.css']
})
export class FlightsReactiveComponent {
  flightForm: FormGroup;
  airports: { code: string, name: string }[] = [
    { code: 'JFK', name: 'John F. Kennedy International Airport' },
    { code: 'LAX', name: 'Los Angeles International Airport' },
    { code: 'ORD', name: 'Hare International Airport' },
    { code: 'DFW', name: 'Dallas Fort Worth International Airport' },
  ];
  passengerOptions: number[] = [1, 2, 3, 4, 5];

  constructor(private fb: FormBuilder) {
    this.flightForm = this.fb.group({
      flightType: ['roundtrip', Validators.required],
      from: ['', Validators.required],
      to: ['', Validators.required],
      depart: ['', Validators.required],
      return: [''],
      passengers: [1, Validators.required]
    });
  }

In the HTML Markup, we use the formGroup directive to bind the form to the flightForm and the ngSubmit directive specifies the method to be called when the form is submitted.

Each form control is bound to the corresponding form control in the flightForm object using the formControlName directive. For example, the radio buttons for selecting the flight type are bound to the flightType form control, the dropdown menus for selecting the origin and destination airports are bound to the from and to form controls, and so on.

We hiding the return date picker based on the value of the flightType form control using the *ngIf directive.

Finally, the submit button is disabled when the form is invalid using the [disabled] property, which is bound to the valid property of the flightForm object.

<form [formGroup]="flightForm" (ngSubmit)="onSubmit()">
  <div>
    <label>Flight:</label>
    <input type="radio" formControlName="flightType" value="roundtrip" />Round
    trip <input type="radio" formControlName="flightType" value="oneway" />One
    way
  </div>
  <div>
    <label>From:</label>
    <select formControlName="from">
      <option *ngFor="let airport of airports" [value]="airport.code">
        {{ airport.name }}
      </option>
    </select>
  </div>
  <div>
    <label>To:</label>
    <select formControlName="to">
      <option *ngFor="let airport of airports" [value]="airport.code">
        {{ airport.name }}
      </option>
    </select>
  </div>
  <div>
    <label>Depart:</label>
    <input type="date" formControlName="depart" />
  </div>
  <div *ngIf="flightForm.get('flightType')?.value === 'roundtrip'">
    <label>Return:</label>
    <input type="date" formControlName="return" />
  </div>
  <div>
    <label>Passengers:</label>
    <select formControlName="passengers">
      <option *ngFor="let i of passengerOptions" [value]="i">{{ i }}</option>
    </select>
  </div>
  <button type="submit" [disabled]="!flightForm.valid">Submit</button>
</form>

Done! the form works the same as the template-driven. The main difference is the FormGroup declaration, use of the Formbuilder, and the initialization of each control with the validators.

I personally don't experience any pain during the process, perhaps because I consistently utilize reactive forms. However, I cannot speak for individuals who are new to the framework or come from a different one, as their experiences may differ.

The Differences:

Template-drivenReactive Forms
Form creation / StructureTemplate syntax, based on the structure of the HTMLDeclaration using FormGroup and FormBuilder
Data bindingtwo-way data binding[(ngModel)]explicit data binding through reactive form controls
Validationvalidation rules in the HTML template using attributes like required and patternprogrammatically using validators provided by the Angular Forms module

Overall, template-driven forms are easier to use and require less code to create, but they offer less flexibility and control than reactive forms.

Reactive forms are more powerful and offer greater control over form behavior, but they require more code and are more complex to set up and use.

Do you want to learn to build complex forms and form controls quickly?

Go to learn the Advanced Angular Forms & Custom Form Control Masterclass by Decoded Frontend.

Learn Advanced Angular Forms Build

What about the testing?

I don't want to finish without adding testing to my code, and show write tests for template-driven and reactive forms is not hard.

Let's start with template-driven, but first configure testbed imports the FlightFormComponent and the FormsModule module; set the test environment by creating a ComponentFixture for the FlightFormComponent and compiling the template.

Next, write some tests:

  1. Validate the form is created by checking that the component variable is truthy.

  2. The submit button is disabled when the form is invalid by setting the form fields to an invalid state and checking that the disabled attribute of the submit button is true.

  3. The submit button is enabled when the form is valid by setting the form fields to a valid state and checking that the disabled attribute of the submit button is false.

  4. The onSubmit() method is called when the form is submitted by setting up a spy on the onSubmit() method, triggering a submit event on the form, and checking that the spy has been called.

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';

import { FlightFormComponent } from './flight-form.component';

describe('FlightFormComponent', () => {
  let component: FlightFormComponent;
  let fixture: ComponentFixture<FlightFormComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ FlightFormComponent ],
      imports: [ FormsModule ]
    })
    .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(FlightFormComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create the form', () => {
    expect(component).toBeTruthy();
  });

  it('should disable the submit button when the form is invalid', () => {
    component.flightType = 'roundtrip';
    component.from = 'JFK';
    component.to = 'LAX';
    component.depart = '2022-03-15';
    component.return = '';
    component.passengers = 2;

    fixture.detectChanges();

    const submitButton = fixture.nativeElement.querySelector('button[type="submit"]');
    expect(submitButton.disabled).toBe(true);
  });

  it('should enable the submit button when the form is valid', () => {
    component.flightType = 'oneway';
    component.from = 'JFK';
    component.to = 'LAX';
    component.depart = '2022-03-15';
    component.passengers = 2;

    fixture.detectChanges();

    const submitButton = fixture.nativeElement.querySelector('button[type="submit"]');
    expect(submitButton.disabled).toBe(false);
  });

  it('should call onSubmit() when the form is submitted', () => {
    spyOn(component, 'onSubmit');
    component.flightType = 'oneway';
    component.from = 'JFK';
    component.to = 'LAX';
    component.depart = '2022-03-15';
    component.passengers = 2;

    fixture.detectChanges();

    const form = fixture.nativeElement.querySelector('form');
    form.dispatchEvent(new Event('submit'));
    expect(component.onSubmit).toHaveBeenCalled();
  });
});

Reactive Forms is close similar, imports the FlightFormComponent and the ReactiveFormsModule module; sets up the test environment by creating a ComponentFixture for the FlightFormComponent and compiling the template.

Next, write the same tests:

  1. The form is created by checking that the form property of the component is falsy (since the form is initially invalid) and that the component variable is truthy.

  2. The submit button is disabled when the form is invalid by setting the form fields to an invalid state and checking that the disabled attribute of the submit button is true.

  3. The submit button is enabled when the form is valid by setting the form fields to a valid state and checking that the disabled attribute of the submit button is false.

  4. The onSubmit() method is called when the form is submitted by setting up a spy on the onSubmit() method, triggering a submit event, and checking that the spy has been called.

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { FlightsReactiveComponent } from './flights-reactive.component';

describe('FlightsReactiveComponent', () => {
  let component: FlightsReactiveComponent ;
  let fixture: ComponentFixture<FlightsReactiveComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ FlightsReactiveComponent],
      imports: [ ReactiveFormsModule ]
    })
    .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(FlightsReactiveComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create the form', () => {
    expect(component.form.valid).toBeFalsy();
    expect(component).toBeTruthy();
  });

  it('should disable the submit button when the form is invalid', () => {
    component.form.controls['flightType'].setValue('roundtrip');
    component.form.controls['from'].setValue('JFK');
    component.form.controls['to'].setValue('LAX');
    component.form.controls['depart'].setValue('2022-03-15');
    component.form.controls['return'].setValue('');
    component.form.controls['passengers'].setValue(2);

    fixture.detectChanges();

    const submitButton = fixture.nativeElement.querySelector('button[type="submit"]');
    expect(submitButton.disabled).toBe(true);
  });

  it('should enable the submit button when the form is valid', () => {
    component.form.controls['flightType'].setValue('oneway');
    component.form.controls['from'].setValue('JFK');
    component.form.controls['to'].setValue('LAX');
    component.form.controls['depart'].setValue('2022-03-15');
    component.form.controls['passengers'].setValue(2);

    fixture.detectChanges();

    const submitButton = fixture.nativeElement.querySelector('button[type="submit"]');
    expect(submitButton.disabled).toBe(false);
  });

  it('should call onSubmit() when the form is submitted', () => {
    spyOn(component, 'onSubmit');
    component.form.controls['flightType'].setValue('oneway');
    component.form.controls['from'].setValue('JFK');
    component.form.controls['to'].setValue('LAX');
    component.form.controls['depart'].setValue('2022-03-15');
    component.form.controls['passengers'].setValue(2);

    fixture.detectChanges();

    const form = fixture.nativeElement.querySelector('form');
    form.dispatchEvent(new Event('submit'));
    expect(component.onSubmit).toHaveBeenCalled();
  });
});

Recap

The idea is to show that both solutions are very good, but in my opinion, reactive forms are ideal for complex, dynamic, or forms requiring complex validation and data handling.

Using template-driven forms is faster and simpler to manage forms than reactive forms. These forms are perfect for handling uncomplicated forms with simple data handling and validation, as they require minimal setup code.

If you want to gain a deeper understanding of each one, check out the following videos. They provide comprehensive insights into Forms within Angular.

ย