Angular Forms new unified control state change events
Tags: angular, frontend, webdev, javascript
Published: 2024-06-10 15:18:49

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:

```ts
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 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:

```ts
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:

```html
<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:

```html
<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:

```ts
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:

```ts
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:

```ts
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 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:

```ts
type FormControlStatus = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED';
```

>**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.

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s4bsgb8lt524s29qyulj.png)

## 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`**.

```ts
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`**.

```ts
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 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](https://www.linkedin.com/in/davide-passafaro/)**. 👋😁