Angular Forms new unified control state change events

davidepassafaro

Davide Passafaro

Posted on June 10, 2024

Angular Forms new unified control state change events

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 of FormControl, its value is an object;
  • FormArray: represents a list of FormControl, 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([])
});
Enter fullscreen mode Exit fullscreen mode

Note: there is also the FormRecord class, an extension of the FormGroup class, which allows you to dynamically create a group of FormControl 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') ]),
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 ]
});
Enter fullscreen mode Exit fullscreen mode

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 the valid 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';
Enter fullscreen mode Exit fullscreen mode

Note: valid, invalid, pending, enabled and disabled properties are indeed just getters derived from the status 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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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 as pristine;
  • markAsDirty(): marks the control as dirty;
  • markAsTouched(): marks the control as touched;
  • markAsUntouched(): marks the control as untouched;
  • markAllAsTouched(): marks the control and its descendant as touched.

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. 👋😁

💖 💪 🙅 🚩
davidepassafaro
Davide Passafaro

Posted on June 10, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related