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 typeFormGroup
Add the
airports
with their respective codes and names, the same with thepassengerOptions
.Inject the Formbuilder in the constructor.
Add the form controls for
flightType
,from
,to
,depart
,return
, andpassengers
, and specifies their initial values and validation rules.Set the
flightType
,from
,to
,depart
, andpassengers
form controls are all marked as required using theValidators.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-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.
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:
Validate the form is created by checking that the
component
variable is truthy.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 istrue
.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 isfalse
.The
onSubmit()
method is called when the form is submitted by setting up a spy on theonSubmit()
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:
The form is created by checking that the
form
property of the component is falsy (since the form is initially invalid) and that thecomponent
variable is truthy.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 istrue
.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 isfalse
.The
onSubmit()
method is called when the form is submitted by setting up a spy on theonSubmit()
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.
Photo by Leon Dewiwje on Unsplash
Top comments (0)