DEV Community

Cover image for Unlock the Power of Angular Reactive Forms
Naira Gezhoyan
Naira Gezhoyan

Posted on

Unlock the Power of Angular Reactive Forms

Angular's Reactive Forms module is a powerful tool for creating and managing complex form logic. It enables the application to respond to user input in a clear, predictable, and scalable manner. One of the core features of Reactive Forms is its Observable-based API, which allows us to build reactive UIs that react to changes over time.

Angular Reactive Forms consist of building blocks such as Form Control, Form Group, and Form Array. These building blocks allow you to create and organize form controls, group related inputs together, and handle multiple inputs dynamically.

Image description

Core Concepts of Reactive Forms

Angular's Reactive Forms are designed around observable streams, providing developers with full control to manage the state of forms at any given moment. Built on a model-driven approach, the structure of these forms is influenced by the underlying data model rather than the user interface (UI).

This article will guide you through the key features of Angular Reactive Forms and offer to provide Angular component code for each aspect.

Exploring the Building Blocks

1. Form Groups: The Foundation

FormGroup is a collection of FormControl objects. It aggregates the values of each child FormControl into one object, with each control name serving as the key.

Below is an example of a component utilizing a FormGroup:

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

@Component({
  selector: 'app-profile-editor',
  template: `
    <form [formGroup]="profileForm">
      <label>
        First Name:
        <input type="text" formControlName="firstName">
      </label>
      <label>
        Last Name:
        <input type="text" formControlName="lastName">
      </label>
    </form>
  `,
})
export class ProfileEditorComponent {
  profileForm = new FormGroup({
    firstName: new FormControl(''),
    lastName: new FormControl(''),
  });
}
Enter fullscreen mode Exit fullscreen mode

2. Form Arrays: Managing Dynamic Inputs

FormArray provides an alternative to FormGroup for managing any number of unnamed controls. Unlike FormGroup's fixed, named controls, FormArrays typically manage controls that are dynamically added.

Here's a component with a FormArray:

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

@Component({
  selector: 'app-profile-editor',
  template: `
    <form [formGroup]="profileForm">
      <div formArrayName="aliases">
        <div *ngFor="let alias of aliases.controls; let i=index">
          <input [formControlName]="i">
        </div>
      </div>
      <button (click)="addAlias()">Add Alias</button>
    </form>
  `,
})
export class ProfileEditorComponent {
  profileForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.profileForm = this.fb.group({
      aliases: this.fb.array([
        this.fb.control('')
      ])
    });
  }

  get aliases() {
    return this.profileForm.get('aliases') as FormArray;
  }

  addAlias() {
    this.aliases.push(this.fb.control(''));
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Form Validation: Ensuring Input Quality

Angular provides a set of built-in validators, and you can also write your own custom validators. Validators are functions that process a FormControl or collection of controls and return an error or null.

Here's how you add validation to a FormControl:

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

@Component({
  selector: 'app-example',
  template: `
    <input [formControl]="name" placeholder="Enter name">
    <p *ngIf="name.invalid && (name.dirty || name.touched)" class="error">
      Name is required.
    </p>
  `,
})
export class ExampleComponent {
  name = new FormControl('', Validators.required);
}
Enter fullscreen mode Exit fullscreen mode

4. Leveraging Observables in Reactive Forms

Each FormControl, FormGroup, and FormArray instance has a valueChanges observable. You can subscribe to this observable to react whenever the control's value changes.

this.profileForm.get('firstName').valueChanges.subscribe(value => {
  console.log('First name has changed to:', value);
});
Enter fullscreen mode Exit fullscreen mode

Observables form the backbone of Angular's Reactive Forms. They provide a mechanism to handle a wide array of scenarios including handling user events, managing asynchronous operations, and maintaining state.

One of the major advantages of using observables in Reactive Forms is their capability to offer a broad range of operators like map, filter, debounceTime, etc., which simplifies handling complex scenarios.

Let's implement a reactive transformation to monitor changes to the firstName field:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
import { User } from './user.model';
import { UserService } from './user.service';
import { Subject } from 'rxjs';
import { debounceTime, filter, switchMap, takeUntil } from 'rxjs/operators';

@Component({
  selector: 'app-user-form',
  template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
  <input formControlName="firstName" placeholder="First Name">
  <input formControlName="lastName" placeholder="Last Name">
  <input formControlName="email" placeholder="Email">
  <input formControlName="age" placeholder="Age">
  <button type="submit">Submit</button>
</form>`
})

export class UserFormComponent implements OnInit, OnDestroy {
  userForm = new FormGroup<User>({
    firstName: new FormControl(''),
    lastName: new FormControl(''),
    email: new FormControl(''),
    age: new FormControl(null),
  });

  userSearchResults = [];
  private destroy$ = new Subject<void>();

  constructor(private userService: UserService) {}

  ngOnInit(): void {
    this.userForm.get('firstName').valueChanges.pipe(
      debounceTime(400),
      filter(value => value.length > 2),
      switchMap(value => this.userService.search(value)),
      takeUntil(this.destroy$)
    ).subscribe(results => {
      this.userSearchResults = results;
    });
  }

  onSubmit(): void {
    if (this.userForm.valid) {
      this.userService.addUser(this.userForm.value);
    }
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}
Enter fullscreen mode Exit fullscreen mode

In this component, we subscribe to changes in the firstName field of the form in the ngOnInit lifecycle method. We use the debounceTime, filter, and switchMap operators to control how often we search based on the user input.

We also use the takeUntil operator to unsubscribe from the valueChanges Observable when the component is destroyed, preventing potential memory leaks.

When the form is submitted, we use the onSubmit method to check the form validity and, if valid, we call userService.addUser with the form value.

Best Practices

When working with Angular Reactive Forms, adhering to certain best practices can lead to more efficient and maintainable code:

  • Immutability: Consider your form's value as if it were immutable. Rather than modifying an existing value, create a new one. This approach allows Angular to efficiently track changes and update the UI.
this.userForm.setValue({...this.userForm.value, firstName: 'New name'});
Enter fullscreen mode Exit fullscreen mode
  • Reactive transformations: Leverage RxJS operators for transforming values reactively. This practice keeps your component code clean, enhances readability, and facilitates complex transformations with simplicity. Here's an example using the map operator to transform the input stream:
this.userForm.get('age').valueChanges.pipe(
  map(value => Math.max(0, value))
).subscribe(value => {
  console.log(value);
});
Enter fullscreen mode Exit fullscreen mode
  • Component-based design: Structure your forms as compositions of components, with each component encapsulating a section of your form. This organization method contributes to a more maintainable and organized codebase.

Break down your form into smaller, manageable components. For example, create a distinct component for the user's personal information:

@Component({
  selector: 'app-personal-info',
  inputs: ['parentForm: formGroup'],
  templateUrl: './personal-info.component.html'
})
export class PersonalInfoComponent {
  @Input()
  parentForm: FormGroup;
}
Enter fullscreen mode Exit fullscreen mode

Then, use it in the parent form like this:

<form [formGroup]="userForm">
  <app-personal-info [parentForm]="userForm"></app-personal-info>
  <!-- Other form components -->
</form>
Enter fullscreen mode Exit fullscreen mode
  • Error handling: Monitor the validation status of your form or individual form controls using the statusChanges Observable. This technique allows for reactive display of error messages in your UI.
this.userForm.get('firstName').statusChanges.pipe(
  filter(status => status === 'INVALID')
).subscribe(() => {
  console.log('First name is invalid');
});
Enter fullscreen mode Exit fullscreen mode
  • Avoiding memory leaks: Always remember to unsubscribe from observables when the component is destroyed to prevent memory leaks. The takeUntil operator offers an easy way to do this.

Don't forget to unsubscribe from the valueChanges Observable when the component is destroyed:

private destroy$ = new Subject<void>();

ngOnInit(): void {
  this.userForm.get('firstName').valueChanges.pipe(
    takeUntil(this.destroy$)
  ).subscribe(value => {
    console.log(value);
  });
}

ngOnDestroy(): void {
  this.destroy$.next();
  this.destroy$.complete();
}
Enter fullscreen mode Exit fullscreen mode
  • Asynchronous validation: Conduct asynchronous validation by returning a Promise or an Observable in your validator function. This allows for the validation of form input against server-side resources.

An example of asynchronous validation, where we check whether a username is taken:

this.userForm.get('username').setAsyncValidators(this.usernameValidator.validate.bind(this.usernameValidator));
Enter fullscreen mode Exit fullscreen mode

The UsernameValidator service might look like this:

@Injectable({ providedIn: 'root' })
export class UsernameValidator {
  constructor(private userService: UserService) {}

  validate(control: AbstractControl): Promise<ValidationErrors|null> | Observable<ValidationErrors|null> {
    return this.userService.isUsernameTaken(control.value).pipe(
      map(isTaken => isTaken ? { usernameTaken: true } : null),
      catchError(() => null)
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

In this validator service, a backend query is made to check if the username is taken, and if it is, a validation error is returned.

Reactive Form Controls in Angular Templates

Angular's Reactive Forms provide a set of directives to link form controls in your templates. Here's how you can link them:

  • formControlName: The directive links a standalone FormControl instance to a form control element.
  • formGroupName: This directive links a FormGroup instance to a DOM element.
  • formArrayName: The directive links a FormArray instance to a DOM element.

FormArray in Angular is a class used when you want to create a dynamic form that permits the user to add or remove inputs or groups of inputs. This feature becomes particularly useful when the exact number of inputs or groups of inputs isn't known in advance.

Consider a form scenario where a user can enter personal details and multiple addresses (home, office, etc.). In this case, the addresses can be represented as a FormArray of FormGroup instances, where each FormGroup represents an address.

Here is a demonstration of how this can be implemented in user-form.component.ts:

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

@Component({
  selector: 'app-user-form',
  templateUrl: './user-form.component.html',
})
export class UserFormComponent implements OnInit {
  userForm: FormGroup;

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.userForm = this.fb.group({
      personalDetails: this.fb.group({
        firstName: ['', Validators.required],
        lastName: ['', Validators.required],
        email: ['', [Validators.required, Validators.email]],
        age: [''],
      }),
      addresses: this.fb.array([
        this.createAddressFormGroup()
      ])
    });
  }

  createAddressFormGroup(): FormGroup {
    return this.fb.group({
      street: ['', Validators.required],
      city: ['', Validators.required],
      state: ['', Validators.required],
      zip: ['', Validators.required],
    });
  }

  get addresses(): FormArray {
    return this.userForm.get('addresses') as FormArray;
  }

  addAddress() {
    this.addresses.push(this.createAddressFormGroup());
  }

  removeAddress(index: number) {
    this.addresses.removeAt(index);
  }

  onSubmit() {
    if (this.userForm.valid) {
      console.log(this.userForm.value);
    } else {
      alert("Form is invalid, please review your information");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

In the above example, addresses is a FormArray that starts with one FormGroup instance for an address. The createAddressFormGroup method constructs a FormGroup for an address. The addresses getter method returns the addresses FormArray. The addAddress and removeAddress methods are used to add and remove FormGroup instances for addresses from the addresses FormArray.

Now, you can modify your form template (user-form.component.html) to display the dynamic addresses form:

<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
  <div formArrayName="addresses">
    <div *ngFor="let address of addresses.controls; let i = index" [formGroupName]="i" class="form-group">
      <h3>Address {{i + 1}}</h3>

      <label>Street:</label>
      <input formControlName="street" class="form-control" placeholder="Street">
      <div *ngIf="address.get('street').errors?.required" class="alert alert-danger">
        Street is required.
      </div>

      <label>City:</label>
      <input formControlName="city" class="form-control" placeholder="City">
      <div *ngIf="address.get('city').errors?.required" class="alert alert-danger">
        City is required.
      </div>

      <label>State:</label>
      <input formControlName="state" class="form-control" placeholder="State">
      <div *ngIf="address.get('state').errors?.required" class="alert alert-danger">
        State is required.
      </div>

      <label>Zip:</label>
      <input formControlName="zip" class="form-control" placeholder="Zip">
      <div *ngIf="address.get('zip').errors?.required" class="alert alert-danger">
        Zip is required.
      </div>

      <button (click)="removeAddress(i)">Remove Address</button>
    </div>

    <button (click)="addAddress()">Add Address</button>
  </div>

  <button type="submit" class="btn btn-primary">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode

In the HTML snippet above, the *ngFor directive is used to loop over the FormGroup instances within the addresses FormArray. The formGroupName directive is used to bind each FormGroup instance to a div element. When the "Remove Address" button is clicked, it triggers the removeAddress method, and the addAddress method is called when the "Add Address" button is clicked.

Note that for each field in the address FormGroup, the error messages use address.get('<field>').errors?.required to check for required validation errors. The address variable here refers to each FormGroup instance within the addresses FormArray.

Conclusion

Leveraging the capabilities of Angular Reactive Forms enables the creation of dynamic, intuitive, and user-friendly web applications. Angular's Reactive Forms module, with its flexibility and efficiency, stands as an indispensable tool for constructing and handling forms in Angular applications. Utilizing features like FormArray and nested FormGroup, you can manage and validate complex, dynamic forms with ease.
Enjoy coding!

Top comments (1)

Collapse
 
younes_achachi profile image
younes achachi

good job , will be better if you put source code somewhere !