Reactive forms have always been a key part of the Angular ecosystem. In version 14, they’ve received a huge developer experience update with the ability to strongly type the form.
Angular provides two different options for dealing with form-based user input: Template-Driven Forms and Reactive Forms.
Template-Driven Forms are a simple directive binding between a form control and a property in a component. Something like:
public firstName: string;
public lastName: string;
and
<label>First Name: </label><input type="text" [(ngModel)]="firstName" />
<label>Last Name: </label><input type="text" [(ngModel)]="lastName" />
This is obviously very simple to set up, but it’s equally limiting. If I want to tie into when either of these values changed, I would likely need to manually tie the event to a method. Then, if I wanted to validate that data, I would need to write my own methods to do that as well as to modify available behavior based on that validation.
Reactive Forms are generally preferred over Template-Driven forms because they allow you to programatically structure your form, provide full event-driven support through RxJs observables, validate individual fields or combinations of fields with built-in validators or with your own synchronous or asynchronous validators, and allow you to build complex forms easily.
One of the most common ways to set up a simple reactive form is by injecting a FormBuilder
instance into a component via the constructor, and then using it to build your form group:
public addressForm: = this.formBuilder.group({
firstName: [''],
lastName: [''],
address1: [''],
address2: [''],
city: [''],
state: [''],
zip: ['']
});
constructor(private formBuilder: FormBuilder) { }
Then, in the template for the component, you can create a form whose inputs will utilize the form group you just created.
<form [formGroup]="addressForm">
<label>First Name: </label><input type="text" formControlName="firstName" />
<label>Last Name: </label><input type="text" formControlName="lastName" />
<label>Address 1: </label><input type="text" formControlName="address1" />
<label>Address 2: </label><input type="text" formControlName="address2" />
<label>City: </label><input type="text" formControlName="city" />
<label>State: </label><input type="text" formControlName="state" />
<label>ZIP: </label><input type="text" formControlName="zip" />
</form>
We could then do something like provide a getter for the person’s name:
public get name(): string {
return `${this.addressForm.get('firstName') ?? ''} ${this.addressForm.get('lastName') ?? ''}`;
}
Or we could attempt to look up an address when the address fields change:
this.addressForm.valueChanges().subscribe((value) => {
this.sampleAddressService.lookUpAddress(value).subscribe((response) => {
// some code based on response.
});
});
This is simple enough, but we’re referencing our form properties without any type-safety. We have to know the name of each form property and won’t know if we’re wrong or we made a typo until run time. Thankfully, Angular 14 has descended from the ether (Angular development community) to bequeath upon us strong typing and type completion.
Let’s propose this form now instead:
personForm = this.formBuilder.group({
firstName: [''],
lastName: [''],
age: [0],
isAlive: [true]
});
Now, we can do things like
this.personForm.getRawValue().firstName
or
this.personForm.controls.isAlive.value
Not only will we get type completion on the field names, TypeScript knows the types of those form fields based on the value provided.
This segment of code:
const isTwentyFive = this.personForm.getRawValue().age === '25'
will throw an error because TypeScript knows that the age
form control on the personForm
form group is numeric, and a string can’t be equal to a number.
This is obviously a trivial example, but imagine managing groups of complex forms across an entire application with large numbers of fields (think a purchase order or your taxes), and the impact of this new feature should become apparent.
I will be writing additional posts on the new Standalone Components that are in dev preview as well as the new streamlined best practices documents that have come with Angular 14.