Angular — Complete Guide to Reactive Forms

Brandon Wohlwend
13 min readNov 5, 2023

--

Imagine you’re having a conversation where you’re instantly responding to each comment. That’s how Reactive Forms work. They’re like a live conversation between your form and your application, enabling immediate and continuous communication of data.

This article should guide us — you and me — through the conpepts of Reactive Forms in Angular. Let’s start with the basics and dive deeper afterwards.

Just some short fact: In Reactive Forms, the state of your form doesn’t change; instead, every change creates a new state, which is like getting a fresh snapshot every time something changes.

FormControl, FormGroup, FormArray, FormBuilder

FormWhat? These concepts are one of the most important things to know regarding reactive Forms. So it is crucial to understand them.

FormControl

A FormControl is the basic building block of Angular's Reactive Forms. It's like an individual input field—just one question on a form. Imagine you have a single textbox asking for the user's name; this textbox would be represented by a FormControl. It keeps an eye on the value of the input field and any validation rules that field needs to follow. Here's what you need to know:

  • Holds the value: It stores what the user types into a form input like a textbox, checkbox, or dropdown.
  • Tracks changes: Anytime the user modifies the input, the FormControl is updated to reflect that change.
  • Manages validation: It also keeps tabs on the validation status (e.g., whether the input is valid, has errors, or is required).

FormGroup

When you have several questions that belong together, you group them, right? That’s what a FormGroup does. It’s a collection of FormControls that represents a form section or the entire form itself. Think of it as a group of fields which are tied together logically, like a form with fields for personal details—first name, last name, and email address would collectively be a FormGroup. Here is what you need to know:

  • Combines controls: It allows you to group multiple FormControls into one unit.
  • Tracks their state: FormGroup watches the state of its controls as a whole, giving you the big picture (like whether all the fields in a section are valid).
  • Values and validity: You can check the overall form value or validation status in one place, rather than individually.

FormArray

What if you want to ask a question several times, such as listing all your siblings? For repeating questions, you use a FormArray. It’s a bit like FormGroup but without the named keys. It’s an array that allows for any number of controls, and they don’t have to be identical. Use FormArray for fields that can be duplicated or when the number of inputs is unknown or dynamic.

  • Manages dynamic quantities: Ideal for identical form fields or controls that can be repeated.
  • Keeps order: Maintains the order of controls, which is vital when the sequence matters.
  • Add or remove controls: You can programmatically add new controls to or remove controls from the FormArray.

FormBuilder

Finally, meet FormBuilder, your handy tool that streamlines the creation of form controls. Handcrafting forms with many controls can be repetitive and verbose. FormBuilder provides convenient methods to handle the heavy lifting, reducing the amount of boilerplate needed to build complex forms.

  • Simplifies creation: Offers shortcuts for generating FormControls, FormGroups, and FormArrays.
  • Reduces redundancy: You write less code and avoid repetition, making your code cleaner and easier to read.
  • Maintains readability: Even though it’s shorthand, the resulting structures are just as readable and understandable.

Now, lets practice this a bit and see it in action!

Set Up your first Reactive form

Before we dive in, make sure you have:

  • Angular CLI installed on your system.
  • A new Angular project created by running ng new your-project-name.
  • Navigated into your project folder with cd your-project-name.

Step 1: Import the ReactiveFormsModule

First things first, we need to invite ReactiveFormsModule to our party. It contains all the tools we need for our form.

Open the app.module.ts file and add the following imports:

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

@NgModule({
declarations: [
// Your components here
],
imports: [
// Other modules here
ReactiveFormsModule // Add this line
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Step 2: Creating the Form Model

Let’s create our form model in the app.component.ts file. Think of this model as the blueprint for our contact form.

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

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
// Create a new FormControl with an initial value of an empty string.
name = new FormControl('');
}

Step 3: Designing the Template

Now, let’s sketch the form in our app.component.html. This is where our FormControl comes to life in the user interface.

<div>
<label for="name">Name:</label>
<input id="name" type="text" [formControl]="name">
</div>

The [formControl] directive binds our name FormControl in the component class to the input element in the template.

Step 4: Save and Sync Data

We want to see the value our user types as they type it. Let’s add a small bit of code below our input to display the form control’s value in real-time.

<div>
<label for="name">Name:</label>
<input id="name" type="text" [formControl]="name">
</div>
<div>
<p>Your name is: {{ name.value }}</p>
</div>

Awesome! But what about FormGroup and FormBuilder? Imagine our contact form needs more information. Let’s add an ‘email’ field and group our inputs together. This is where FormGroup comes into play, like putting together a puzzle where each piece is a form control.

Step 5: Create a FormGroup

In your app.component.ts, let's refactor our code to include a FormGroup:

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

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
// We're creating a FormGroup which is a group of FormControls.
contactForm = new FormGroup({
name: new FormControl(''), // Our existing name FormControl
email: new FormControl('') // A new FormControl for the email
});
}

Step 6: Update the Template with FormGroup

Modify app.component.html to use the FormGroup. Here's how to connect the dots:

<form [formGroup]="contactForm">
<div>
<label for="name">Name:</label>
<input id="name" type="text" formControlName="name">
</div>
<div>
<label for="email">Email:</label>
<input id="email" type="email" formControlName="email">
</div>
<button type="submit" [disabled]="!contactForm.valid">Submit</button>
</form>

Notice how we use formGroup to bind our template to the contactForm group, and formControlName to link each input to its respective FormControl within the group.

Using FormBuilder to Simplify Form Creation

Typing out new FormControl and FormGroup instances manually can be a chore, especially for complex forms. FormBuilder provides a more succinct way to create the same structures.

Step 7: Refactor with FormBuilder

Inject FormBuilder into your component and use it to create the form group:

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

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
contactForm: FormGroup;

constructor(private fb: FormBuilder) {
// Using FormBuilder to create the FormGroup.
this.contactForm = this.fb.group({
name: [''], // Define the default value and validators inside the array
email: ['']
});
}
}

The fb.group() method is a shorthand provided by FormBuilder that does the heavy lifting of creating the FormGroup and its internal FormControls.

Step 8: Submitting the Form

Let’s also handle form submission. Add a method to handle the submit event in the app.component.ts:

submit() {
console.log(this.contactForm.value);
}

Then, update your form in app.component.html to call the submit() method when the form is submitted:

<form [formGroup]="contactForm" (ngSubmit)="submit()">
<!-- Form inputs stay the same -->
<button type="submit" [disabled]="!contactForm.valid">Submit</button>
</form>

Working with Form Controls

In this section, we’ll get our hands dirty by linking HTML elements to Reactive Form controls, tweaking form controls without a user’s input, and deciphering the myriad states a form control can be in. This is where the Reactive Forms really begin to shine — providing you with the superpowers to interact and react to user inputs in real time.

Linking HTML Form Elements to Reactive Form Controls

When your form structure in Angular matches your HTML, you can create a seamless link between the two using the formControlName directive. This attribute tells Angular which FormControl within a FormGroup should be associated with which input.

Let’s say you have a FormGroup with controls for user details:

this.userDetailsForm = this.fb.group({
firstName: [''],
lastName: [''],
age: ['']
});

In your template, you’d link each input field to its respective FormControl like so:

<form [formGroup]="userDetailsForm">
<input formControlName="firstName" placeholder="First Name">
<input formControlName="lastName" placeholder="Last Name">
<input type="number" formControlName="age" placeholder="Age">
</form>

The formControlName directive binds each input to the FormControl instance with the corresponding name in the FormGroup.

Programmatically Updating Form Controls

Sometimes, you may want to change the value of a form control from your code, like setting a default value or clearing a form after submission. Angular gives you methods to do just that:

// To set a new value for the firstName FormControl
this.userDetailsForm.get('firstName').setValue('Jane');

// To reset the form to its initial state
this.userDetailsForm.reset();

Remember to handle these actions responsibly. For example, you don’t want to overwrite user input unexpectedly, so it’s best to perform programmatic updates when the user expects them, such as after submitting the form or when a certain condition in the application is met.

The Life and Times of a FormControl

A FormControl is not just about holding a value; it has a life—a series of states it transitions through as a user interacts with your form. These states give you a peek into the user's interaction pattern and help you provide feedback. Here’s a rundown:

  • value: Holds the current value of the form control. It’s what the user has typed in or what you’ve set programmatically.
  • valid: A boolean that tells you if the input conforms to your validation rules. If you’ve said “Emails only!” and the user types “Just a name,” valid will be false.
  • pristine / dirty: pristine means untouched—no value has been entered yet. Once the user starts typing, the control becomes dirty.
  • touched / untouched: These relate to focus. If a user has clicked into a form field, it’s touched; if they haven’t, it’s untouched.
  • enabled / disabled: Indicates whether the form control is active or not. A disabled control won’t be included in the form’s value.
  • errors: If your form control doesn’t meet validation criteria, errors will contain any validation errors. It’s null if there are no errors.

You can access these properties directly in your component class or use them in your template to give users immediate feedback. For example:

<input formControlName="firstName">
<div *ngIf="userDetailsForm.get('firstName').errors">
Please enter a valid first name.
</div>

Validation and Error Handling

A form without validation is like a door without a lock — functional, yet not secure. Validation ensures that the input we receive is within the guidelines we require. Angular’s Reactive Forms module makes adding validation logic a streamlined process.

Applying Validators to Form Controls and Form Groups

Validators are like the bouncers of the form world: they ensure only the right data gets through.

Angular provides a set of built-in validators that you can easily apply to your form controls. For example, if you need an input to be required and a valid email, you would set it up like this:

import { FormBuilder, Validators } from '@angular/forms';

// ...

this.userDetailsForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]]
});

You can apply validators to entire FormGroups as well, which is useful for cross-field validation:

this.userDetailsForm = this.fb.group({
// ... (other form controls)
}, { validator: yourCrossFieldValidatorFunction });

How to Communicate with Users When Things Go Wrong

No one likes being told they’ve made a mistake, but with clear, constructive feedback, we can guide users to correct them. Here’s how you can display validation messages in your form:

<form [formGroup]="userDetailsForm">
<!-- Email input -->
<div>
<input formControlName="email">
<div *ngIf="userDetailsForm.get('email').errors?.required">
Email is required.
</div>
<div *ngIf="userDetailsForm.get('email').errors?.email">
Please enter a valid email address.
</div>
</div>
<!-- ... (other inputs and validation messages) -->
</form>

Using Angular’s built-in directives, we can check for specific errors and display messages accordingly.

Crafting Your Own Set of Rules

Sometimes, the built-in validators don’t cover all the bases. For instance, if you want to validate that a password contains a mix of characters and numbers, you might need a custom validator:

import { AbstractControl, ValidationErrors } from '@angular/forms';

function customPasswordValidator(control: AbstractControl): ValidationErrors | null {
const valid = /^(?=.*[a-zA-Z])(?=.*[0-9])/.test(control.value);
return valid ? null : { 'invalidPassword': true };
}

And you would use it like any other validator:

this.userDetailsForm = this.fb.group({
password: ['', [Validators.required, customPasswordValidator]]
});

Observables and Form Changes

The ‘Reactive’ in Reactive Forms isn’t just for show — it’s Angular’s way of letting you tap into a form’s changes in real-time, reacting to user inputs as they happen. This is made possible through the use of Observables from the RxJS library. Let’s dive into how these Observables can become a game-changer for your forms.

Understanding Observables in Reactive Forms

Imagine your form as a river, and every user interaction is a ripple in the water. Observables allow you to ‘watch’ the river and react whenever those ripples occur. In technical terms, an Observable provides a stream of values over time that you can subscribe to, much like tuning into a radio station.

In the context of Reactive Forms, every form control has a valueChanges and a statusChanges observable that emits data whenever the value or validation status of the control changes.

Subscribing to Form Value Changes

To make your application responsive to user input, you can subscribe to the valueChanges observable of a form control or a form group:

this.userDetailsForm.valueChanges.subscribe(values => {
console.log('Form values changed to:', values);
});

With this subscription, your callback function will run every time a user changes a value in the form, providing you with the updated values.

Utilizing valueChanges and statusChanges Observables

These observables can be employed for a variety of tasks, such as filtering a list of suggestions as the user types, enforcing business rules, or even throttling rapid-fire inputs to prevent performance issues.

Responding to Value Changes

For example, suppose you want to auto-fill a city field based on a zip code:

this.userDetailsForm.get('zipCode').valueChanges.subscribe(zip => {
// Call a function to retrieve city information based on the zip
this.lookupCity(zip).then(city => {
this.userDetailsForm.get('city').setValue(city, { emitEvent: false });
});
});

By using { emitEvent: false }, you prevent the setValue from triggering another valueChanges event, thus avoiding a potential infinite loop.

Tracking Form Validity

Monitoring statusChanges can be particularly useful for enabling or disabling form submission based on validity:

this.userDetailsForm.statusChanges.subscribe(status => {
this.isFormValid = status === 'VALID';
});

With this pattern, isFormValid will update in real-time, reacting immediately to any and all validity changes in the form.

Dynamic Forms with FormArrays

When you’re dealing with a list of items in a form — like multiple phone numbers for a contact — you’ll need a dynamic solution that Reactive Forms provide: the FormArray. This is where Angular truly shines, offering the flexibility to create forms that can grow and shrink on the fly.

The Concept of a FormArray

A FormArray is a bit like a FormGroup, but instead of handling named controls, it's a list of controls that can be identical or different. Think of FormArray as an array that holds FormGroup or FormControl objects. This is particularly useful when the number of controls is determined at runtime, such as user-generated forms.

When to Use a FormArray

You should consider using FormArray when:

  • You have a variable number of similar controls (like multiple phone fields).
  • The data structure you are representing is an array.
  • You want to add or remove controls dynamically based on user interaction.

Managing FormArray: Adding and Removing Controls

Creating a FormArray is just like making a FormGroup. Here's a quick setup:

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

// ...

phoneNumbers = new FormArray([]);

// To initialize with some phone numbers, you might do:
for(let i=0; i < initialPhoneCount; i++) {
this.phoneNumbers.push(new FormControl(''));
}

Adding New Controls

Imagine you have a button that allows users to add new phone number fields:

addPhoneNumber() {
this.phoneNumbers.push(new FormControl(''));
}

With each call to addPhoneNumber(), a new FormControl is dynamically added to the FormArray.

Removing Controls

And just as you can add to a FormArray, you can also remove controls. Here's how you might remove a phone number at a specific index:

removePhoneNumber(index: number) {
this.phoneNumbers.removeAt(index);
}

By calling removePhoneNumber(), you can dynamically remove the control at the given index, effectively updating the form to reflect the user's intention.

Practical Example: Building a Registration Form

A user registration form is a common feature of many applications. It captures information from the user and is an excellent example of how Angular’s Reactive Forms can be employed in a real-world scenario.

Step 1: Planning the Form Structure

Before writing any code, let’s outline what a typical registration form might need:

  • Name (first and last)
  • Email
  • Password (with confirmation)
  • Address (with multiple fields like street, city, state, zip code)
  • Agree to terms and conditions checkbox

Step 2: Setting Up the Form Model with FormGroup and FormControl

Start by creating a new FormGroup in your component, and define the form controls within:

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

@Component({
// component metadata
})
export class UserRegistrationComponent {
registrationForm = new FormGroup({
firstName: new FormControl('', Validators.required),
lastName: new FormControl('', Validators.required),
email: new FormControl('', [Validators.required, Validators.email]),
password: new FormControl('', [Validators.required, Validators.minLength(6)]),
confirmPassword: new FormControl('', Validators.required),
address: new FormGroup({
street: new FormControl(''),
city: new FormControl(''),
state: new FormControl(''),
zip: new FormControl('', Validators.pattern(/^\d{5}$/))
}),
terms: new FormControl(false, Validators.requiredTrue)
});

// Additional component properties and methods
}

Step 3: Connecting the Form Model to the Template

In your template, you’ll bind the form group to a <form> element using the [formGroup] directive, and each input will be tied to a form control name:

<form [formGroup]="registrationForm" (ngSubmit)="onSubmit()">

<!-- Name fields -->
<input formControlName="firstName" placeholder="First Name">
<input formControlName="lastName" placeholder="Last Name">

<!-- Email field -->
<input type="email" formControlName="email" placeholder="Email">

<!-- Password fields -->
<input type="password" formControlName="password" placeholder="Password">
<input type="password" formControlName="confirmPassword" placeholder="Confirm Password">

<!-- Address fields wrapped in a nested group -->
<div formGroupName="address">
<input formControlName="street" placeholder="Street">
<input formControlName="city" placeholder="City">
<input formControlName="state" placeholder="State">
<input formControlName="zip" placeholder="Zip">
</div>

<!-- Terms and conditions checkbox -->
<label>
<input type="checkbox" formControlName="terms">
Agree to terms and conditions
</label>

<!-- Submit button -->
<button type="submit" [disabled]="!registrationForm.valid">Register</button>
</form>

Step 4: Adding Validation and Error Handling

Under each input, add a method to display error messages if the field is touched and invalid. Here’s an example for the email field:

<span *ngIf="registrationForm.get('email').touched && registrationForm.get('email').hasError('required')">
Email is required.
</span>
<span *ngIf="registrationForm.get('email').touched && registrationForm.get('email').hasError('email')">
Please enter a valid email address.
</span>

Step 5: Handling Form Submission

Create a method to handle form submissions. This method should check the form’s validity and then proceed with the registration process (usually involving calling an API):

onSubmit() {
if (this.registrationForm.valid) {
console.log('Form Submitted', this.registrationForm.value);
// API call to register the user...
}
}

With these steps, you’ve now built a fully functional, reactive user registration form with Angular. This form dynamically validates user input, provides instant feedback, and handles submissions, embodying the principles of Reactive Forms.

For now, thats it. That were the most important concepts on reactive forms with Angular and you can already start creating your own. I hope you enjoyed it! If you dont want to miss any articles like these, please consider following me :)

--

--

Brandon Wohlwend

Mathematician | Data Science, Machine Learning | Java, Software Engineering