DEV Community

Cover image for Why You Should Use Strictly Typed Reactive Forms in Your Angular App
Dany Paredes
Dany Paredes

Posted on

Why You Should Use Strictly Typed Reactive Forms in Your Angular App

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);
  }
}


Enter fullscreen mode Exit fullscreen mode

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('')
  });


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

Next, using the get method, update the amount field using get and patchValue



this.purchaseForm.get('amount')?.patchValue(priceWithTax)


Enter fullscreen mode Exit fullscreen mode

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; }>'


Enter fullscreen mode Exit fullscreen mode

The error is because the amount field expects a string value, so parse the to string.



this.purchaseForm.get('amount')?.patchValue(priceWithTax.toString())


Enter fullscreen mode Exit fullscreen mode

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(),
})


Enter fullscreen mode Exit fullscreen mode

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)
  });


Enter fullscreen mode Exit fullscreen mode

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(),
         ~~~~~~


Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

Angular 14 provides a new property nonNullable option to tell the number not to be null.



amount: new FormControl<number>(0, { nonNullable: true})


Enter fullscreen mode Exit fullscreen mode

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>;
}> {
}


Enter fullscreen mode Exit fullscreen mode

Next, assign the interface type for the form.



  purchaseForm!: PurchaseFormModel;


Enter fullscreen mode Exit fullscreen mode

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),
      }
    )
  }


Enter fullscreen mode Exit fullscreen mode

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>;
}> {
}


Enter fullscreen mode Exit fullscreen mode

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)
      }
    )
  }


Enter fullscreen mode Exit fullscreen mode

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
Learn Advanced Angular Forms Build

Photo by Markus Spiske on Unsplash

Top comments (0)