Skip to content

A Guide to (Typed) Reactive Forms in Angular - Part I (The Basics)

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

When building a simple form with Angular, such as a login form, you might choose a template-driven approach, which is defined through directives in the template and requires minimal boilerplate. A barebone login form using a template-driven approach could look like the following:

<!-- login.component.html -->
<form name="form" (ngSubmit)="myAuthenticationService.login(credentials)">
    <label for="email">E-mail</label>
    <input type="email" id="email" [(ngModel)]="credentials.email" required email />
    <label for="password">Password</label>
        <input type="password" id="password" [(ngModel)]="credentials.password" required />
    <button type="submit">Login!</button>
</form>
// login.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html'
})

export class LoginComponent implements OnInit {
  public credentials = {
    email: '',
    password: ''
  };

  constructor(public myAuthenticationService: MyAuthenticationService) { }
}

However, when working on a user input-heavy application requiring complex validation, dynamic fields, or a variety of different forms, the template-driven approach may prove insufficient. This is where reactive forms come into play.

Reactive forms employ a reactive approach, in which the form is defined using a set of form controls and form groups. Form data and validation logic are managed in the component class, which updates the view as the user interacts with the form fields. This approach requires more boilerplate but offers greater explicitness and flexibility.

In this three-part blog series, we will dive into the reactive forms data structures, learn how to build dynamic super forms, and how to create custom form controls.

In this first post, we will familiarize ourselves with the three data structures from the @angular/forms module:

FormControl

The FormControl class in Angular represents a single form control element, such as an input, select, or textarea. It is used to track the value, validation status, and user interactions of a single form control. To create an instance of a form control, the FormControl class has a constructor that takes an optional initial value, which sets the starting value of the form control. Additionally, the class has various methods and properties that allow you to interact with and control the form control, such as setting its value, disabling it, or subscribing to value changes.

As of Angular version 14, the FormControl class has been updated to include support for typed reactive forms - a feature the Angular community has been wanting for a while. This means that it is now a generic class that allows you to specify the type of value that the form control will work with using the type parameter <TValue>. By default, TValue is set to any, so if you don't specify a type, the form control will function as an untyped control.

If you have ever updated your Angular project with ng cli to version 14 or above, you could have also seen an UntypedFormControl class. The reason for having a UntypedFormControl class is to support incremental migration to typed forms. It also allows you to enable types for your form controls after automatic migration.

Here is an example of how you may initialize a FormControl in your component.

import { FormControl } from '@angular/forms';

const nameControl = new FormControl<string>("John Doe");

Our form control, in this case, will work with string values and have a default value of "John Doe".

If you want to see the full implementation of the FormControl class, you can check out the Angular docs!

FormGroup

A FormGroup is a class used to group several FormControl instances together. It allows you to create complex forms by organizing multiple form controls into a single object. The FormGroup class also provides a way to track the overall validation status of the group of form controls, as well as the value of the group as a whole.

A FormGroup instance can be created by passing in a collection of FormControl instances as the group's controls. The group's controls can be accessed by their names, just like the controls in the group.

As an example, we can rewrite the login form presented earlier to use reactive forms and group our two form controls together in a FormGroup instance:

// login.component.ts
import { FormControl, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html'
})

export class LoginComponent implements OnInit {
  public form = new FormGroup({
    email: new FormControl<string>('', [Validators.required, Validators.email]),
    password: new FormControl<string>('', [Validators.required]),
  });

  constructor(public myAuthenticationService: MyAuthenticationService) { }

  public login() {

    // if you hover over "email" and "password" in your IDE, you should see their type is inferred
    console.log({
      email: this.form.value.email,
      password: this.form.value.password
    });

    this.myAuthenticationService.login(this.form.value);
  }

}
<!-- login.component.html -->
<form name="form" (ngSubmit)="login()" [formGroup]="form">
    <label for="email">E-mail</label>
    <input type="email" id="email" formControlName="email" />
    <label for="password">Password</label>
        <input type="password" id="password" formControlName="password" />
    <button type="submit">Login!</button>
</form>

Notice we have to specify a formGroup and a formControlName to map the markup to our reactive form. You could also use a formControl directive instead of formControlName, and provide the FormControl instance directly.

FormArray

As the name suggests, similar to FormGroup, a FormArray is a class used to group form controls, but is used to group them in a collection rather than a group.

In most cases, you will default to using a FormGroup but a FormArray may come in handy when you find yourself in a highly dynamic situation where you don't know the number of form controls and their names up front.

One use case where it makes sense to resort to using FormArray is when you allow users to add to a list and define some values inside of that list. Let's take a TODO app as an example:

import { Component } from '@angular/core';
import { FormArray, FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-todo-list',
  template: `
    <form [formGroup]="todoForm">
      <div formArrayName="todos">
        <div *ngFor="let todo of todos.controls; let i = index">
          <input [formControlName]="i" placeholder="Enter TODO name" />
        </div>
      </div>
      <button (click)="addTodo()">Add TODO</button>
    </form>
  `,
})
export class TodoListComponent {
  public todos = new FormArray<FormControl<string | null>>([]);

  public todoForm = new FormGroup({
    todos: this.todos,
  });

  addTodo() {
    this.todoForm.controls['todos'].push(new FormControl<string>(''));
  }
}

In both of the examples provided, we instantiate FormGroup directly. However, some developers prefer to pre-declare the form group and assign it within the ngOnInit method. This is usually done as follows:

// login.component.ts
import { FormControl, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html'
})

export class LoginComponent implements OnInit {
  // predeclare the form group
  public form: FormGroup;

  constructor(public myAuthenticationService: MyAuthenticationService) { }

  ngOnInit() {
    // assign in ngOnInit
    this.form = new FormGroup({
      email: new FormControl<string>('', [Validators.required, Validators.email]),
      password: new FormControl<string>('', [Validators.required]),
    });
  }

  public login() {
    // no type inference :(
    console.log(this.form.value.email);
  }

}

If you try the above example in your IDE, you'll notice that the type of this.form.value is no longer inferred, and you won't get autocompletion for methods such as patchValue. This is because the FormGroup type defaults to FormGroup<any>. To get the right types, you can either assign the form group directly or explicitly declare the generics like so:

public form: FormGroup<{
  email: FormControl<string>,
  password: FormControl<string>,
}>;

However, explicitly typing all your forms like this can be inconvenient and I would advise you to avoid pre-declaring your FormGroups if you can help it.

In the next blog post, we will learn a way to construct dynamic super forms with minimal boilerplate.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co