I continue to play with the new features of Angular 14/15, and one pending task is to learn about Typed Reactive Forms. The strict forms help us avoid many common issues when working with our forms.
The best way to learn and understand why to use Typed Reactive forms is by showing a scenario. I continue with the project 'Using Functional Guards In Angular'.
Scenario
In our scenario, we need to add a new field in the purchaseForm
, the field amount
, and increase it to 0.5
, then submit the form.
export class RegisterComponent {
purchaseForm = new FormGroup({
name: new FormControl(''),
email: new FormControl(''),
});
sendForm() {
console.log(this.registerForm.value);
}
}
The Solution
First, add the amount formControl
linked in the HTML Markup with input with controlName
.
The code looks like this:
purchaseForm = new FormGroup({
name: new FormControl(''),
email: new FormControl(''),
amount: new FormGroup('')
});
HTML Markup
<form [formGroup]="purchaseForm" (ngSubmit)="sendForm()">
<label for="first-name">Name: </label>
<input id="first-name" type="text" formControlName="name">
<label for="email">Email</label>
<input id="email" type="email" formControlName="email">
<label for="amount">Amount</label>
<input id="amount" type="number" formControlName="amount">
*TAX 0.5
<button type="submit">Send</button>
</form>
Add Taxes
Before sending the form, we need to increase the value because the amount is a string. We must convert it to a number with the Number()
function.
Declare the variable priceWithTax
to store the result of the operation.
const priceWithTax = Number(this.purchaseForm.controls.amount.value) + 0.5;
Next, using the get method, update the amount field using get
and patchValue
this.purchaseForm.get('amount')?.patchValue(priceWithTax)
But I got an error.
src/app/components/register/register.component.ts:17:5
17 amount: new FormGroup('')
~~~~~~~~~~~~~~~~~~~~~~~~~
The expected type comes from property 'amount', which is declared here on type 'Partial<{ name: string | null; email: string | null; amount: string; }>'
The error is because the amount
field expects a string value, so parse the to string.
this.purchaseForm.get('amount')?.patchValue(priceWithTax.toString())
It works, but the code is a bit fragile and unclear.
Problems
We use the get
method, passing a string to get the amount field it compiles but getting the error in runtime.
The PatchValue method is better because it proposes the available properties in the form.
this.purchaseForm.patchValue({
amount: priceWithTax.toString(),
})
Some questions come to my head.
Why do I need to convert the amount? It is a number :(
What happens if reset the form, the new value of the amount is null :(
How do I turn on my form more strictly?
Most of these problems do not exist anymore with Typed Forms.
Convert To Typed Forms
The Reactive Type Forms give us better control and stricter template form checks. It helps complex forms and deeply nested control with type-safety API.
Let's move to convert my current form to the new reactive strict forms.
FormControls
The FormControl support generic types. We can set the specific type for each field.
purchaseForm = new FormGroup({
name: new FormControl<string>(''),
email: new FormControl<string>(''),
amount: new FormControl<number>(0)
});
So, the amount field is a number, so we don't need to convert the value to a number anymore.
In compilation, throw an error because it requires a number value.
Error: src/app/components/register/register.component.ts:25:7 - error TS2322: Type
'string' is not assignable to type 'number'.
25 amount: priceWithTax.toString(),
~~~~~~
Remove the toString()
and get another error in compilation because the amount should be null.
Error: src/app/components/register/register.component.ts:23:25 - error TS2531: Obj
ect is possibly 'null'.
23 const priceWithTax =this.purchaseForm.controls.amount.value + this.PURCHASE
_TAX;
Angular 14 provides a new property nonNullable
option to tell the number not to be null.
amount: new FormControl<number>(0, { nonNullable: true})
Perfect, we already move the controls to strict types next to FormGroup.
FormGroup
The FormGroup supports generics types so that we can declare all fields required in my forms, like an interface extending from FormGroup
.
export interface PurchaseFormModel extends FormGroup<{
name: FormControl<string>;
email: FormControl<string>;
amount: FormControl<number>;
}> {
}
Next, assign the interface type for the form.
purchaseForm!: PurchaseFormModel;
Finally, use the formBuilder to create the form for each property.
The form definition must match the interface and doesn't allow adding extra properties.
constructor(private fb: FormBuilder) {
this.purchaseForm = this.fb.group(
{
name: this.fb.nonNullable.control('hello'),
email: this.fb.nonNullable.control('demo@demo.com'),
amount: this.fb.nonNullable.control(0),
}
)
}
The form declaration requires all fields to be not null, so use them this.fb.nonNullable.control
if we want to add a nullable field, like cookies. Add in the interface and set type string and null.
export interface PurchaseFormModel extends FormGroup<{
name: FormControl<string>;
email: FormControl<string>;
amount: FormControl<number>;
cookies: FormControl<boolean | null>;
}> {
}
The field in the form builder uses control with the default value.
constructor(private fb: FormBuilder) {
this.purchaseForm = this.fb.group(
{
name: this.fb.nonNullable.control('hello'),
email: this.fb.nonNullable.control('demo@demo.com'),
amount: this.fb.nonNullable.control(0),
cookies: this.fb.control(true)
}
)
}
Yeah, We have the form strict and matching with our interface.
One More Thing
If we reset the form, it uses the default value in the form controls, not null, as before.
Conclusion
It was a small slide about Type Reactive Forms in Angular 14/15, check out the code or read more about in the following links:
One more thing:
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
Photo by Markus Spiske on Unsplash
Top comments (0)