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.
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(''),
});
}
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(''));
}
}
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);
}
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);
});
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();
}
}
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'});
-
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);
});
- 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;
}
Then, use it in the parent form like this:
<form [formGroup]="userForm">
<app-personal-info [parentForm]="userForm"></app-personal-info>
<!-- Other form components -->
</form>
-
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');
});
-
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();
}
-
Asynchronous validation: Conduct asynchronous validation by returning a
Promise
or anObservable
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));
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)
);
}
}
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");
}
}
}
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>
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)
good job , will be better if you put source code somewhere !