Angular Forms: Choosing Between Reactive and Template Driven Forms
Tags: angular, forms, javascript, html
Published: 2023-03-13 08:50:37

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:

![Form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nryzdga0h4p78u1z32ng.gif)

> 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`.

```bash
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.

```javascript
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.

```xml
<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.

```bash
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".
    

```javascript
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.

```xml
<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-driven | Reactive Forms |
| --- | --- | --- |
| **Form creation / Structure** | Template syntax, based on the structure of the HTML | Declaration using FormGroup and FormBuilder |
| **Data binding** | two-way data binding`[(ngModel)]` | explicit data binding through reactive form controls |
| **Validation** | validation rules in the HTML template using attributes like `required` and `pattern` | programmatically 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.](https://courses.decodedfrontend.io/courses/advanced-angular-forms)

### 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.
    

```typescript
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.
    

```typescript
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.

* 👉🏽[Repo: https://github.com/danywalls/reactive-forms-vs-template-forms](https://github.com/danywalls/reactive-forms-vs-template-forms)
    
* 👉🏽 [**Advanced Angular Forms by Dmytro**](https://www.youtube.com/watch?v=cWXhaFBGArk)
    
* 👉🏽[Prefer Template-Driven Forms](https://www.youtube.com/watch?v=L7rGogdfe2Q)
    
* 👉🏽 [Handling Angular Forms Without Losing Your Sanity](https://www.youtube.com/watch?v=iqXiic6hE9U)

Photo by Leon Dewiwje on Unsplash