{"community_link":"https://community.indepth.dev/t/how-to-use-the-updateon-option-in-angular-forms/821"}

The updateOn Option in Angular Forms

When using Angular Forms, by default, on every keystroke, the values of your form controls are updated and their associated validators are executed. This may not be always desirable. The updateOn option gives us a finer-grained control over the moment when value updates and validators are triggered.

The updateOn Option in Angular Forms

The validation of user inputs plays a crucial role when dealing with forms. It prevents the user from submitting incomplete or invalid data. This ensures they don't get frustrated by having to submit the same invalid form over and over. In Angular forms, validation takes the shape of functions that get passed in the user inputs, and tells us whether the user-entered data is correct or not.

By default, on every keystroke, the values of our form controls are updated. These validator functions then get executed. This may not be always desirable. Sometimes we want a finer-grained control over the moment when value updates and validators are triggered. This is where Angular forms' updateOn option comes into play.

In this article, we will explain what the updateOn option is, why it's important, and uncover how to use it to avoid degrading our Angular applications' performance.

If we want to understand the problem and the value proposition of the updateOn option, we first need to understand how Angular forms work. Let's say we have a bare-bones HTML form in our UI with no Angular involved:

<form>
  <label>
    Full Name
    <input type="text" name="fullName"/>
  </label>
  <label>
    Email
    <input type="email" name="email"/>
  </label>
  <button type="submit">Submit</button>
</form>

This form contains two inputs and a submit button. The first input—fullName—represents the user's full name, and the second input—email—is the user's email address. How do we handle this form in Angular?

One goal, two techniques

To handle this form in Angular, we have two different techniques at our disposal: template-driven and reactive. In the template-driven approach, the responsibility of the form is put inside the HTML template, whereas in the reactive approach, it's the component class who is in charge of the form. So, these two techniques are fundamentally different in the way the developer wires up the form. But under the hood they use the same foundations, and achieve the same goal which is to track:

  • the value the user has entered in the UI: the raison d'être of the form, what we wanted to capture by displaying the form to the user in the first place;
  • what interactions the user has with the form inputs;
  • whether errors exist on the form inputs or not.

To achieve this goal, Angular provides building blocks that define a form model. What's a form model, and what are these building blocks? Let's find out!

The form model

The form model is the data structure used by Angular to represent an HTML form. This is what does the glue between the form elements in the HTML and Angular. To create this form model, Angular gives us three building blocks that are used by both template-driven and reactive forms:

  • The FormControl class: tracks the state of an individual input element.
  • The FormGroup class: tracks the state of a related group of form controls.
  • The FormArray class: tracks state of an array of related FormControls and FormGroups.

So, a form model is composed of instances of FormControl, FormGroup and FormArray classes.

For the signup form above, the form model could look something like this:

FormGroup -> 'signUpFormGroup'
  FormControl -> 'fullName'
  FormControl -> 'email'

The three fundamental Angular forms' building blocks share a lot of behavior and base functionality that Angular has abstracted out in a class called AbstractControl. So FormControl,  FormGroup, and FormArray are all concrete instances of AbstractControl.

Learn more about the AbstractControl class in the InDepth.dev blog.


The form model in reactive forms

In the reactive approach, we create the form model ourselves in the component class. For our signup form example, the form model could be created like this:

signUpFormGroup = new FormGroup(
  {
    fullName: new FormControl(''),
    email: new FormControl('')
  }
);

We then use reactive forms directives (formControl, formControlName, formGroup, formGroupName, formArrayname) to tie the form model to the HTML input elements like this:

<form [formGroup]="signUpFormGroup" (ngSubmit)="submit(signUpFormGroup)">
  <label>
    Full Name
    <input type="text" formControlName="fullName">
  </label>
  <label>
    Email
    <input type="email" formControlName="email">
  </label>
  <button type="submit">Submit</button>
</form>

The form model  in template-driven forms

In the template-driven approach, we don't explicitly create a form model. The form model is automagically created by Angular when we use template-driven form directives (ngForm, ngModel, and ngModelGroup) in our HTML like this:

<form #signUpForm="ngForm" (ngSubmit)="submit(signUpForm.form)">
  <label>
    Full Name
    <input type="text" [(ngModel)]="user.fullName">
  </label>
  <label>
    Email
    <input type="text" [(ngModel)]="user.email">
  </label>
  <button type="submit">Submit</button>
</form>
In the template-driven approach, we can still have access to the underlying form model by exporting the ngForm directive into a template reference variable. To do that, we use the #signUpForm="ngForm" syntax. signUpForm.form with then contains our form model.

Now that we understand what the form model is, how it's created, and the role it plays in Angular forms, let's talk about what happens when the user interacts with our form inputs in the UI.

DOM events that update FormControl instances

It is important to note that native HTML form elements—<input/>, <textarea>, <select>—always deal with a single value. The user always interacts with individual form elements in UI. Therefore, they are always bound to a FormControl. We never bind them to a FormGroup or a FormArray. Those are logical groupings of FormControls. Nothing more.

Whether you're using reactive forms or template-driven forms, Angular keeps in sync the values of the native DOM input elements (the view) and their associated Angular counterparts, the FormControl instances (the model).
When the view is updated i.e., when the user types a value into the <input/> element, the  <input/> element emits an input event. This leads to two main actions on the FormControl instance:

  • the FormControl's value property is updated,
  • the validator functions associated with the FormControl are executed.

The problem we are trying to solve with the updateOn option

Here is an example of a FormControl without the updateOn option. We've attached some Angular built-in validators—required and email—to the control:

import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-root',
  template: `
    <form [formGroup]="signUpFormGroup" (ngSubmit)="submit(emailFormControl)">
      <label>
        Full Name:
        <input [formControl]="emailFormControl" type="email" placeholder="Enter Your Email Address" #inputElement />
      </label>
      <div *ngIf="emailFormControl.invalid && (emailFormControl.touched || emailFormControl.dirty)">
        <div *ngIf="emailFormControl.errors.required">
          This field is required
        </div>
        <div *ngIf="emailFormControl.errors.email">
          Please provide a valid email address
        </div>
      </div>
      <button type="submit">Submit</button>
    </form>
    <pre>
      Native Input Element Value: {{ inputElement.value }}
      Form Control Value: {{ emailFormControl.value }}
      Form Control Status: {{ emailFormControl.status }}
      Form Control Touched: {{ emailFormControl.touched }}
      Form Control Dirty: {{ emailFormControl.dirty }}
    </pre>
  `
})
export class AppComponent {
  emailFormControl = new FormControl('', {
    validators: [Validators.required, Validators.email]
  });
  signUpFormGroup = new FormGroup({
    email: this.emailFormControl
  });

  submit(emailFormControl: FormControl) {
    console.log(emailFormControl);
  }
}

Try the live StackBlitz demo:

As you can see, on every keystroke, the state of our form model is updated. This corresponds to the input event on the DOM <input/> element being emitted. This DOM input event is what triggers FormControl updates. The validator functions get executed and the error messages are updated immediately. So, by default, the validator functions get invoked too often.

When using async validators, the input data is typically sent to a backend server to check its validity. So, you will be sending an HTTP request on every keystroke. Even if your backend is very robust and can handle the charge, this is an unnecessary waste of resources. So, the default validation timing in Angular form is problematic when using server side validation.

We can do better than this!

The updateOn option in reactive forms

As you can see every time the value of a form control changes, Angular reruns our validators. This can lead to serious performance issues. To alleviate this problem, the v5 release of Angular has introduced the updateOn property on the AbstractControl. This means that FormControl, FormGroup, and FormArray, all have this property.

The updateOn option allows us to set the update strategy of our form controls by choosing which DOM event trigger updates. The possible values for the updateOn property are:

  • change, the default: corresponds to the DOM input event of the <input/> element;
  • blur: corresponds to the DOM blur event of the <input/> element;
  • submit: corresponds to the DOM submit event on the parent form.

The updateOn option on a FormControl

To set the updateOn option on a FormControl instance, we use the long form of its constructor's second parameter which is of type AbstractControlOptions:

interface AbstractControlOptions {
    validators?: ValidatorFn | ValidatorFn[] | null;
    asyncValidators?: AsyncValidatorFn | AsyncValidatorFn[] | null;
    updateOn?: 'change' | 'blur' | 'submit';
}

Let's see some examples.

Changing the update strategy to 'blur'

Let's now take our above FormControl example and set the updateOn property to 'blur'.

  emailFormControl = new FormControl('', {
    validators: [Validators.required, Validators.email],
    updateOn: 'blur'
  });

Try the live Stackblitz demo:

As you can see, updating the <input/> element won't trigger any validation messages until the blur event occurs on the <input/> element i.e., when it loses the focus. This minimizes the number of times our validation functions are executed.

Changing the update strategy to 'submit'

Let's now set the update strategy to 'submit' for our FormControl.

  emailFormControl = new FormControl('', {
    validators: [Validators.required, Validators.email],
    updateOn: 'submit'
  });

Try the live Stackblitz demo:

Now, neither the input event nor the blur even on the  <input/> element will trigger any validation messages. The FormControl will update itself only when the parent form is submitted.

The updateOn option on a FormGroup or a FormArray

FormGroup and FormArray are subclasses of AbstractControl. Therefore, they also support the updateOn option. If you set the updateOn property of a FormGroup or a FormArray, that value will be used as the default value for the updateOn property on all its child controls. But, if a child control explicitly sets its own value for the updateOn option, that explicit value will take precedence.

Let's dissect the following example:

signUpFormGroup = new FormGroup(
  {
    fullName: new FormControl('', {
      updateOn: 'blur'
    }),
    email: new FormControl('')
  },
  { updateOn: 'submit' }
);

Try the live Stackbliz demo:

We use updateOn: 'submit' on the FormGroup level and updateOn: 'blur' on the fullName FormControl. With this, the fullName control will update only when the corresponding input loses focus. For the  email control, the updates only happen when the parent form is submitted.

This is equivalent to the following code:

signUpFormGroup = new FormGroup({
  fullName: new FormControl('', { updateOn: 'blur' }),
  email: new FormControl('', { updateOn: 'submit' })
});

If your form contains many form controls, setting the updateOn option on each and every form control can be tedious. This is why the ability to set the updateOn property on the FormGroup or FormArray level is very handy.

It's important to note that the updateOn property has no impact on when the FormGroup or FormArray updates happen. It only affects the child controls. For example, if the update strategy of  FormGroup or FormArray is set to 'blur', and its children use the 'change' update strategy, the FormGroup or FormArray will still update on 'change' with its children.

Setting the updateOn option using the FormBuilder API

The updateOn option is also available when using the FormBuilder API to create our form. Below is an example of such usage:

signUpFormGroup = this.fb.group(
  {
    fullName: this.fb.control('', {updateOn: 'blur'}),
    email: this.fb.control('')
  },
  {updateOn: 'submit'}
);

Dynamically changing the value of the updateOn option

How can we dynamically the value of the updateOn option? Angular doesn't provide a way to set the updateOn option after the form controls has been created. With reactive forms,  the only way to set the updateOn option is in the constructor of the FormControl, FormGroup, or FormArray classes.

I guess this is because dynamically changing the value of the updateOn option is unlikely to happen, and the Angular team didn't want to add a method setUpdateOn just for the sake of adding it. If you really want to change the value of the updateOn option in a form control, your only option is to create a new form control and to replace the previous one.

The  updateOn option in template-driven forms

When using template-driven forms, we don't instantiate directly the form model. Luckily, Angular provides us with the ngModelOptions on the NgModel directive that we can use to pass options to the underlying FormControl that it generates.

The updateOn option on a ngModel

For example, to set the updateOn option to 'blur', we can do something like this:

<input type="text" [(ngModel)]="user.email" [ngModelOptions]="{updateOn: 'blur'}">

Similarly, this is how we set the update strategy it to 'submit' in template-driven forms:

<input type="text" [(ngModel)]="user.email" [ngModelOptions]="{updateOn: 'submit'}">

The updateOn option at the form level

We can also set the updateOn option at the form level using the ngFormOptions input of the NgForm directive. For example:

<form [ngFormOptions]="{updateOn: 'blur'}">...</form>

With this, 'blur' will be used as the value for the updateOn option of all the child controls of this form unless they explicitly sets their own value for the updateOn option using the ngModelOptions.

Conclusion

In this blog post, we learned about the updateOn option in both template-driven and reactive forms. We saw that, by default, the form model gets updated too often which can degrade our application's performance if it's heavy on forms validation.

Thanks to the updateOn option we can use less aggressive update strategies in our Angular forms. This little option can have a huge positive impact in the performance of your Angular applications.

If you want to learn more about Angular forms in general, check out the article "A thorough exploration of Angular Forms" in the InDepth.dev blog.