How to use ControlValueAccessor to enhance date input with automatic conversion and validation

Post Editor

In this article, we will learn how we can extend native date input through a directive so that it supports conversion of value and validation on value.

6 min read
2 comments
post

How to use ControlValueAccessor to enhance date input with automatic conversion and validation

In this article, we will learn how we can extend native date input through a directive so that it supports conversion of value and validation on value.

post
post
6 min read
2 comments
2 comments

Overall idea behind this article to explain and demonstrate the usage of ControlValueAccessor and Validator interfaces. The former is used to bind together a FormControl from Forms package and native DOM elements. The latter is used to implement validation logic. They can exist independently of each other, but in this article we’ll implement both using a single directive. Our directive will add the following functionality to the application:

  1. Conversion between input value and control value
  2. Validation for invalid date

If you’re using ControlValueAccessor for the first time, I would recommend going through this article first: Never again be confused when implementing ControlValueAccessor in Angular forms.

Conversion of values
Link to this section

We will first create a directive and handle conversion between UI and control value.

<>Copy
// src/app/directives/date-input.directive.ts import { Directive } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @Directive({ selector: 'input[type=date]', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: DateInputDirective, multi: true } ] }) export class DateInputDirective implements ControlValueAccessor { constructor() {} writeValue(obj: any): void {} registerOnChange(fn: any): void {} registerOnTouched(fn: any): void {} }

We mainly did 3 things for directive:

  1. Set the selector to input[type=date] - This will add the conversion mechanism and validations to all date-inputs without any extra effort.
  2. Defined DateInputDirective class as Value Accessor through the NG_VALUE_ACCESSOR token. Our directive will be used by Angular to set-up synchronisation with FormControl.
  3. implemented the ControlValueAccessor interface

For this example, we are only concerned with two methods on the interface:

  1. writeValue - this method is used to write a value to the native DOM element. Simply put, this can be utilised to convert FormControl value to UI value.
  2. registerOnChange - This method will help us to store a function, which will be called by value-changes on the UI. In simpler terms, with this we can convert UI value to FormControl value.

This illustration from the linked article above demonstrates this mechanism:

This illustration from the linked article above demonstrates this mechanism:
This illustration from the linked article above demonstrates this mechanism

Writing value to the native DOM element with writeValue
Link to this section

We want to show the correct and formatted date on the UI when it gets updated through FormControl.

For example, let’s assume that we are getting ISO string of date from our API, which looks something like this: 1994-11-05T08:15:30-05:00, and when we set the value for FormControl bound to the date input, we want the input[type=date] to display date in correct format.

The code below demonstrates how we convert the ISO string into YYYY-MM-DD before we set the resulting value for the native HTML date input:

<>Copy
// src/app/directives/date-input.directive.ts import { formatDate } from '@angular/common'; // ... export class DateInputDirective implements ControlValueAccessor { writeValue(dateISOString: string): void { const UIValue = formatDate(dateISOString, 'YYYY-MM-dd', 'en-IN'); this._renderer.setAttribute( this._elementRef.nativeElement, 'value', UIValue ); } }

Here’s what’s going on above:

  1. We are creating a string called UIValue, which will hold the date in `YYYY-MM-DD` format. As you can see, we have used the formatDate function from @angular/common to get the formatted date.
  2. And then, we are setting input’s value attribute using Renderer2

Let’s quickly try out the above changes:

<>Copy
// src/app/app.component.ts @Component({ selector: 'app-root', templateUrl: './app.component.html', }) export class AppComponent { fg = new FormGroup({ date: new FormControl(new Date().toISOString()), }); get date() { return this.fg.get('date'); } }

Note 2 things in above code:

  1. We set the current date’s ISO string in date FormControl, ideally you would get it from some API.
  2. We created a date getter to get the FormControl. In a reactive form, you can always access any form control through the get method on its parent group, but sometimes it's useful to define getters as shorthand for the template.
<>Copy
<!-- src/app/app.component.html --> <form [formGroup]="fg"> <input type="date" id="birthDate" formControlName="date" /> <div> <code> <b>Control Value: </b>{{ date.value }} </code> </div> </form>

If you look at the output now, it is setting the correct date in input:

Output after setting writeValue
Output after setting writeValue

Getting value from the native DOM element with registerOnChange
Link to this section

The DOM element holds the value as formatted date. When the user updates the value, we'll need to convert it to a valid ISO string.

Let’s add a HostListener first:

<>Copy
// src/app/directives/date-input.directive.ts export class DateInputDirective implements ControlValueAccessor { @HostListener('input', ['$event.target.valueAsNumber']) onInput = (_: any) => {}; // ... }

We are using $event.target.valueAsNumber to read the value. valueAsNumber returns the timestamp in milliseconds, the reason we are using it is because it will help us directly get date using new Date(valueAsNumber). Also notice the onInput function above, it’s just a skeleton for now.

It’s time to implement the conversion logic in registerOnChange:

<>Copy
// src/app/directives/date-input.directive.ts // … export class DateInputDirective implements ControlValueAccessor { // … registerOnChange(fn: (_: any) => void): void { this.onInput = (value: number) => { fn(this.getDate(value).toISOString()); }; } }

registerOnChange is called just once by Angular, passing us the callback named fn in the code above. We can use this callback to update the FormControl value as a reaction to DOM element update. And we are calling it on the input event of date-input through onInput function.

We also need to create a couple of helper functions, you can change them as per your need:

<>Copy
getDate(value: number) { if (value) { const dateObj = new Date(value); return this.isValidDate(dateObj) ? dateObj : { toISOString: () => null }; } return { toISOString: () => null }; } isValidDate(d: Date | number | null) { return d instanceof Date && !isNaN(d as unknown as number); }

Let’s look at the output now:

Output after setting registerOnChange
Output after setting registerOnChange

As you can see, it’s updating the control's value with a valid ISO string.

Validation
Link to this section

Now we will add the validation part so that date-input supports validation out-of-the box.

We will first add NG_VALIDATORS in providers:

<>Copy
// src/app/directives/date-input.directive.ts // … @Directive({ selector: 'input[type=date]', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: DateInputDirective, multi: true, }, { provide: NG_VALIDATORS, useExisting: DateInputDirective, multi: true, }, ], })

Next, we will implement the Validator interface and add validate method:

<>Copy
export class DateInputDirective implements ControlValueAccessor, Validator { // ... validate(control: AbstractControl): ValidationErrors | null { const date = new Date(control.value); return control.value && this.isValidDate(date) ? null : { date: true }; } }

Angular will call the validate method whenever the value of the control changes.

Let’s modify the template to utilize validation:

<>Copy
<form [formGroup]="fg"> <input type="date" id="birthDate" formControlName="date" /> <div class="invalid-feedback" *ngIf="(date?.touched || date?.dirty) && date?.invalid"> Invalid Date </div> </form>

As you can see, we added a div to show a validation message. It uses the date getter defined in the component class.

Let’s understand the *ngIf 's expression:

<>Copy
*ngIf="(date?.touched || date?.dirty) && date?.invalid"
  1. We don’t want to show validation message if user has not interacted with the input, so we have added date?.touched || date?.dirty
  2. We also don’t want to show if date is valid, so we added date?.invalid

You can read more about Validating form input on Angular docs.

Let’s look at the output now:

Output after implementing Validator
Output after implementing Validator

Conclusion

We learned below:

How to use ControlValueAccessor to

  • Convert UI value to valid ISO string and attach it to form-control’s value
  • Convert form-control’s ISO string value to `YYYY-MM-DD` format and update the same on UI

How to use Validator to validate user input for date

You can find the code on Stackblitz and GitHub.

Thanks for reading!

Comments (2)

authorjagrodal
7 June 2021

Thanks for the article! I want my custom CVA component to be marked red also when an external validator is invalid (i.e. Validators.required set on the FormControl when building the form), not only based on the internal validate() method. What is the best way to achieve this?

authorshhdharmen
7 June 2021

You can access all the validation errors through FormControl.errors.

So, in our example, let's say you want to add class red to the input if Validators.required is failed, you would do something like below:

<input
        type="date"
        class="form-control"
        id="birthDate"
        formControlName="date"
        [class.red]="(date?.touched || date?.dirty) && date?.invalid && date?.errors?.required"
      />

Notice: this class binding: [class.red]="(date?.touched || date?.dirty) && date?.invalid && date?.errors?.required".

authorkingjordan
8 June 2021

great article, thanks!

How would you suggest implementing the base ControlValueAccessor functions when using strict mode in angular?

for example all of these variations will fail the compile build due to strict mode being on because you can't use any or Functions as types:

  public registerOnChange(onChange: any): void {
    this.onChange = onChange;
  }

  public registerOnChange(fn: Function): void {
    this.onChange = onChange;
  }

in addition, using this empty function for the callback does not work either because strict mode does not allow empty functions

  onTouched = () => {};
authorshhdharmen
9 June 2021

Take a look at how i have done in the article, it should look something like below:

registerOnChange(fn: (_: any) => void): void {
  this.onInput = (value: number) => {
    fn(this.getDate(value).toISOString());
  };
}

Share

About the author

author_image

I am a Front-end Developer. I like to work on Angular, React, Bootstrap, CSS, SCSS & Electron. I also love to contribute to Open-Source Projects and sometime write articles.

author_image

About the author

Dharmen Shah

I am a Front-end Developer. I like to work on Angular, React, Bootstrap, CSS, SCSS & Electron. I also love to contribute to Open-Source Projects and sometime write articles.

About the author

author_image

I am a Front-end Developer. I like to work on Angular, React, Bootstrap, CSS, SCSS & Electron. I also love to contribute to Open-Source Projects and sometime write articles.

NxAngularCli
NxAngularCli
NxAngularCli

Featured articles