The release of Angular v18 brought a bunch of exciting new features and improvements to the framework.
One of these features is particularly promising, as it introduces a new capability within the Angular Forms library, by enhancing the AbstractControl
class with unified control state change events.
As usual in my articles, before delving into the main topic, let's first review some fundamentals. This will help you better grasp the upcoming contents.
Angular Reactive Forms: the fundamentals
Angular Reactive Forms offer a model-driven approach to handling form inputs, providing synchronous access to the data model, powerful tools for inputs validation and change tracking through Observables
.
The Reactive Forms data model is composed using the following classes:
-
FormControl
: represents a single form input, its value is a primitive; -
FormGroup
: represents a group ofFormControl
, its value is an object; -
FormArray
: represents a list ofFormControl
, its value is an array.
A common example of form can be represented by a FormGroup
like this:
import { FormGroup, FormControl, FormArray } from '@angular/forms';
const articleForm = new FormGroup({
title: "new FormControl(''),"
content: new FormControl(''),
tags: new FormArray([])
});
Note: there is also the
FormRecord
class, an extension of theFormGroup
class, which allows you to dynamically create a group ofFormControl
instances.
All these classes, hereafter referred to just as controls, are derived from the AbstractControl
class, and thus share common properties and methods.
Template binding
Angular Reactive Forms model-driven approach is powered by various directives provided by the library itself, which facilitate the integration of form controls with HTML elements.
Let's take the following FormGroup
as an example:
this.articleForm = new FormGroup({
author: new FormGroup({
name: new FormControl(''),
}),
tags: new FormArray([ new FormControl('Angular') ]),
});
You can easily bind it to the template using the provided directives:
<form [formGroup]="articleForm">
<div formGroupName="author">
<input formControlName="name" />
</div>
<div formArrayName="tags">
<div *ngFor="let tag of tags.controls; index as i">
<input [formControlName]="i" />
</div>
</div>
</form>
What is important to remember, without delving into an exhaustive but out-of-scope explanation, is that the FormGroupDirective
allows us to easily create a button to reset the form and a button to submit its value:
<form [formGroup]="articleForm">
<!-- form template -->
<button type="reset">Clear</button>
<button type="submit">Save</button>
</form>
The FormGroupDirective
intercepts the click events emitted by these buttons to trigger the control's reset()
function, which resets the control to its initial value, and the directive's ngSubmit
output event.
Listening for value changes
In order to listen for value changes to perform custom operations, you can subscribe to the valueChanges
observable of the control you want to track:
myControl.valueChanges.subscribe(value => {
console.log('New value:', value)
});
Disabled controls
Each control can be set to disabled, preventing users from editing its value. This mimics the behavior of the HTML disabled
attribute.
To accomplish this, you can either create a control as disabled, or use the disable()
and enable()
and functions to toggle this status:
import { FormControl } from '@angular/forms';
const myControl = new FormControl({ value: '', disabled: true });
console.log(myControl.disabled, myControl.enabled) // true, false
myControl.enable();
console.log(myControl.disabled, myControl.enabled) // false, true
myControl.disable();
console.log(myControl.disabled, myControl.enabled) // true, false
As you can notice in the example above, the AbstractControl
class provides two dedicated properties to describe this status: disabled
and enabled
.
Validators
To enforce specific rules and ensure that your controls meet certain criteria, you can also specify some validation rules, or validators.
Validators can be synchronous, such as required
or minLength
, or asynchronous, to handle validation that depends on external resources:
import { FormControl, Validators } from '@angular/forms';
import { MyCustomAsyncValidators } from './my-custom-async-validators.ts';
const myFormControl = new FormControl('', {
validators: [ Validators.required, Validators.minLength(3) ],
asyncValidators: [ MyCustomAsyncValidators.validate ]
});
Based on these rules, the AbstractControl
class provides also some properties that describe the status of the validity:
-
valid
: a boolean indicating whether the control value passed all of its validation rules tests; -
invalid
: a boolean indicating whether the control value passed all of its validation rules tests; It is the opposite of thevalid
property; -
pending
: a boolean indicating whether the control value is in the process of conducting a validation check.
FormControlStatus
Both the disabled status and the validation status are interconnected.
In fact, they are derived by the status
property, which is typed as follows:
type FormControlStatus = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED';
Note:
valid
,invalid
,pending
,enabled
anddisabled
properties are indeed just getters derived from thestatus
property 🤓
Pristine and Touched
The AbstractControl
class provides also several properties that describe how the user has interacted with the form:
-
pristine
: a boolean indicating whether the control is pristine, meaning it has not yet been modified; -
dirty
: boolean indicating whether the control has been modified; -
untouched
: a boolean indicating whether the control has not yet been touched, meaning that it has not been interacted with yet; -
touched
: a boolean indicating whether the control has been touched.
Now that we've revisited some of the fundamentals of Angular Reactive Forms, it's finally time to introduce the main topic of this article.
New unified control state change events
Starting from Angular v18, the AbstractControl
class now exposes a new events
observable to track all control state change events.
Thanks to this, you can now monitor FormControl
, FormGroup
and FormArray
classes through the following events: PristineEvent
, PristineEvent
, StatusEvent
and TouchedEvent
.
myControl.events
.pipe(filter((event) => event instanceof PristineChangeEvent))
.subscribe((event) => console.log('Pristine:', event.pristine));
myControl.events
.pipe(filter((event) => event instanceof ValueChangeEvent))
.subscribe((event) => console.log('Value:', event.value));
myControl.events
.pipe(filter((event) => event instanceof StatusChangeEvent))
.subscribe((event) => console.log('Status:', event.status));
myControl.events
.pipe(filter((event) => event instanceof TouchedChangeEvent))
.subscribe((event) => console.log('Touched:', event.touched));
These capabilities are very powerful, especially because, apart from the valueChange
, it was previously not easy to properly track the state changes.
Additionally to this, the FormGroup
class can also emit two additional events through the events
observable: FormSubmittedEvent
and FormResetEvent
.
myControl.events
.pipe(filter((event) => event instanceof FormSubmittedEvent))
.subscribe((event) => console.log('Submit:', event));
myControl.events
.pipe(filter((event) => event instanceof FormResetEvent))
.subscribe((event) => console.log('Reset:', event));
Both the FormSubmittedEvent
and FormResetEvent
are inherited by the FormGroupDirective
and are, in fact, emitted only by the directive itself.
Additional insights
Thanks to this new addition, the following AbstractControl
methods have been updated to support the emitEvent
parameter:
-
markAsPristine()
: marks the control aspristine
; -
markAsDirty()
: marks the control asdirty
; -
markAsTouched()
: marks the control astouched
; -
markAsUntouched()
: marks the control asuntouched
; -
markAllAsTouched()
: marks the control and its descendant astouched
.
Thanks for reading so far 🙏
I’d like to have your feedback so please leave a comment, like or follow. 👏
Then, if you really liked it, share it among your community, tech bros and whoever you want. And don’t forget to follow me on LinkedIn. 👋😁
Top comments (0)