Angular version 18 has introduced a new event emitter called events. It offers more control over the data flow, by allowing for tracking precisely which control is a source of changes, and form state giving access to the form submit and reset events.

The event field is implemented inside the AbstractControl class and is available to all classes that inherit from it: FormControl, FormGroup, FormRecord, and FormArray.

Value and state events of a single form control

Let’s check how the events field works in practice. The code below presents an example component with one FormControl object named nameCtrl.

import { Component, OnInit } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";

@Component({
  selector: "app-form-control-events",
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <label>Name: </label>
    <input type="text" name="name" [formControl]="nameCtrl" />
  `,
})
export class FormControlEventsComponent implements OnInit {
  nameCtrl = new FormControl<string>("");

  ngOnInit(): void {
    this.nameCtrl.events.subscribe((event) => {
      console.log(event);
    });
  }
}

The control is assigned to the name <input> element. There is also a subscription to the events field, to log events emitted by the control. Now, let’s type some value into it. Let it be a single character like 'x'.

What we see in the console is that the event object has emitted three events:

Emitted events logged into the browser console

Three? Wait… We’ll see the fourth one when we blur the <input> element.

Emitted events logged into the browser console

Let's describe each event and describe what it does. Event field emitted four types of events: ValueChangeEvent, StatusChangeEvent, PristineChangeEvent, and TouchedChangeEvent. Each of them extends the ControlEvent abstract class. All of them contain two fields. First is the source field. It points to the object that emitted the event. In our case, events’s source property points to nameCtrl. The second field depends on the event type, and contains a value of the control, or value of the control’s state.

Now, regarding the example above:

ValueChangeEvent - similar to (old fashion ;)) valueChange event. It provides the current value of the control. By default, it is triggered whenever the value of the control has been changed.

StatusChangeEvent - similar to statusChanges. It provides the current validation status. Like ValueChangeEvent, StatusChangeEvent is triggered every time the value of the control has been changed, no matter if the control has assigned validators or not. If the control has an async validator assigned, then StatusChangeEvent will be emitted twice. Once, when the status property is set to PENDING, then for the second time, when the validator returns the result of the validation.

PristineChangeEvent - is triggered when the control value is changed for the first time, regardless of whether the control had an initial value.

TouchedChangeEvent - is triggered when the user interacts with the control for the first time. Usually, it’s triggered when the user blurs the control, even if the value doesn't change.

Since we now know which user actions trigger the event, let’s take a look at how it works with the Reactive Forms API. As we all know, we can change any control's value and status by utilizing methods like setValue, disable, etc. Using them, we can change the default behavior with options properties like onlySelf, emitEvent, and so on. Now we can also determine if ControlEvents should be emitted or not. Methods like markAsTouched, markAllAsTouched (for groups and arrays), markAsDirty, markAsPending, markAsPristine, and markAsUntouched can be called with the option’s emitEvent property. If those methods are called without it, or emitEvent is set to true, then the ControlEvents will be emitted. Otherwise, they will not. But there is a trick. It does not mean the state of the control will not be changed! It will be. The only difference here is that the event will not be triggered. Just take a look at the code below:

const control = new FormControl("Lorem ipsum");
// control.touched -> false

control.markAsTouched({ emitEvent: false });
// control.touched -> true

The value of the touched property has been changed anyway.

Value and state events of a group of controls

So far we have covered ControlEvents in terms of FormControl. It’s time to talk about more advanced structures like groups, and arrays. As we all know, all of those classes inherit from AbstractControl. It means all of them will emit ControlEvents. When it comes to groups and arrays, the ControlEvents object reveals its true power. Until version 18 we had access to the newly emitted value or state of the group/array, but we didn’t know which control was the origin of the change. Now, we can take advantage of ControlEvents and target a specific control inside the subscription. It’s super useful when we want to react to a particular control change. Let’s dive into an example:

import { Component, OnInit } from "@angular/core";
import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";

@Component({
  selector: "app-form-group-events",
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `<form [formGroup]="form">
    <label>Name: </label>
    <input type="text" name="name" formControlName="name" />

    <br /><br />

    <label>Surname: </label>
    <input type="text" name="surname" formControlName="surname" />

    <br /><br />
    <label>Age: </label>
    <input type="number" name="age" formControlName="age" />
  </form>`,
})
export class FormGroupEventsComponent implements OnInit {
  form = new FormGroup({
    name: new FormControl("John"),
    surname: new FormControl("Doe"),
    age: new FormControl(30),
  });

  ngOnInit(): void {
    this.form.events.subscribe((event) => {
      console.log(event);
    });

    this.form.get("name")?.setValue("Jane");
  }
}

As a result, we’ll get two events emitted: ValueChangeEvent and StatusChangeEvent. Both provide an event object that contains a source field, which points directly to the changed control. The second field depends on the event type, and it can be a value of the control/group or the value of its state. The tricky part is that the source field always points to the control that value/state has been changed. The second field is always related to the object we are subscribing to. It’s even more vivid when we work with more advanced structures.

The example below shows a form with nested FormGroup objects. It also contains four subscriptions: to the form root, to the address field, and to the street and city fields within the address.

import { Component, OnInit } from "@angular/core";
import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";

@Component({
  selector: "app-form-group-events",
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `<form [formGroup]="form">
    <label>Name: </label>
    <input type="text" name="name" formControlName="name" />

    <br /><br />

    <label>Surname: </label>
    <input type="text" name="surname" formControlName="surname" />

    <br /><br />

    <label>Age: </label>
    <input type="number" name="age" formControlName="age" />
+
+    <br /><br />
+
+    <fieldset formGroupName="address">
+      <legend>Address:</legend>
+
+      <label>Street: </label>
+      <input type="text" name="street" formControlName="street" />
+
+      <br /><br />
+
+      <label>City: </label>
+      <input type="text" name="city" formControlName="city" />
+    </fieldset>
  </form> `,
})
export class FormGroupEventsComponent implements OnInit {
  form = new FormGroup({
    name: new FormControl("John"),
    surname: new FormControl("Doe"),
    age: new FormControl(30),
+    address: new FormGroup({
+      street: new FormControl("Collins Street"),
+      city: new FormControl("Fox River"),
+    }),
  });

  ngOnInit(): void {
    this.form.get("address.street")?.events.subscribe((event) => {
      console.log(event);
    });

+    this.form.get("address.city")?.events.subscribe((event) => {
+      console.log(event);
+    });
+
+    this.form.get("address")?.events.subscribe((event) => {
+      console.log(event);
+    });
+
+    this.form.events.subscribe((event) => {
+      console.log(event);
+    });
  }
}

Changing the value of the city field will trigger events emitter assigned to the city field and all its parents (yes, we can prevent this propagation within the onlySelf option passed to setValue method, but it's not a case here).

The image below shows how ValueChangeEvent is propagated through the tree of the form, and what data is put into the event object on each level of the form.

Schema of events propagation

Submit and reset events

So far we have covered how the events field behaves in terms of a single FormControl and structures like FormGroup values and states. But that’s not all. Unified Control State Change Events brings long-awaited features, which are submit and reset events. It means no more workarounds, no more submit and reset buttons listeners!

Let’s add this functionality to our example.

...
@Component({
  selector: 'app-form-group-events',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `<form [formGroup]="form">
    <label>Name: </label>
    <input type="text" name="name" formControlName="name" />

    <br/><br/>

    <label>Surname: </label>
    <input type="text" name="surname" formControlName="surname" />

    <br/><br/>

    <label>Age: </label>
    <input type="number" name="age" formControlName="age" />

    <br/><br/>

    <fieldset formGroupName="address">
      <legend>Address:</legend>

      <label>Street: </label>
      <input type="text" name="street" formControlName="street" />

      <br/><br/>

      <label>City: </label>
      <input type="text" name="city" formControlName="city" />
    </fieldset>
+
+   <br/>
+
+    <button type="submit">Submit</button>
+    &nbsp;
+    <button type="reset">Reset</button>
  </form>`,
})

Our form is complete now:

The view of the example form

Let’s trigger the submit event by clicking on the Submit button, and see what the console logged. Unlike events, we have been talking about before, FormSubmittedEvent contains only source property, which points to the main form object. To be more precise, it points to the object assigned to the formGroup directive: <form [formGroup]="form">.

Two things need to be explained in detail. First, the emission of the FormSubmittedEvent is not related to the actual state of the form. It means the event will be emitted every time we click on the submit button, regardless of whether the form has already been submitted. The second thing is the origin of FormSubmittedEvent. It gets triggered in response to the native forms submit event. It means the <form> element has to contain a submit button. The button can be defined explicitly by setting the type attribute to 'submit', or it can be a button without a type (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#type).

The last event I’m going to cover is FormResetEvent. As the name implies, this event is emitted every time the form is reset. Similar to FormSubmittedEvent, the FormResetEvent object contains only the source property. What is interesting is that the reset process is a little bit more complex. By resetting we assume all of the control values and states are going to be set (or revert if you prefer) to its initial values. It means every control (and groups of course) is going to emit three events: TouchedChangeEvent, ValueChangeEvent, and StatusChangeEvent. The form root object will emit one more event - PristineChangeEvent, to ensure that the reset process is completed. After that, the root object will emit the FormResetEvent event.
`

Assuming, we have two subscriptions (all the rest are not necessary for this example), one to the address field, and the second one to the root object, we can observe the entire process of the form resetting.

Let’s take a look at the image below. The first section of events is related to the address field, whilst the second one is related to the main form object.

List of emitted events triggered from the view

The last thing worth mentioning is that the FormResetEvent (at the time of writing this article) is emitted only when we reset the form from the view. Calling the reset method will trigger all events, but reset:

List of emitted events triggered from the forms API

Getting the event we want

The event object emits lots of events every time we change a value. For most of the cases, we are willing to listen only for one of two event types. For this we can embrace the rxjs filter operator to filter out events we are not interested in, and focus only on the desired, like ValueChangeEvent. The code snippet demonstrates this operation in practice.

form.events
  .pipe(filter((event) => event instanceof ValueChangeEvent))
  .subscribe((event) => {
    console.log(event);
  });

Conclusion

Let's summarize what we have learned today. Control State Change Events were introduced in Angular v18. The main purpose of it is to unify events emitted by the form controls. The benefits of using them are:

  • There’s no need to create (and maintain) two subscriptions, one to valueChange, and the second to statusChanges

  • You can react when a user starts interaction with the form, by making it dirty and touched, as pristine and touched status can be observed.

  • From now on, it’s not necessary to listen to the submit and reset buttons clicks, forms API allows us to work with those events in a reactive way.

  • You have access to the control that is a source of the change


Tagged in:

Angular 18, Articles

Last Update: June 26, 2024