Using Strictly Typed Reactive Forms in Angular

Improving Maintainability with Strictly Typed Angular Reactive Forms

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.

Learn Advanced Angular Forms Build