01 Jun 2021
5 min

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

Overall idea behind this article to explain and demonstrate the usage of [ControlValueAccessor](https://angular.io/api/forms/ControlValueAccessor) and [Validator](https://angular.io/api/forms/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

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

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

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:

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:

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

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:

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:

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:

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

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

We will first add [NG_VALIDATORS](https://angular.io/api/forms/NG_VALIDATORS) in providers:

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:

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:

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

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

Frequently asked questions

How do I use ControlValueAccessor with a native date input in Angular?

Create a directive with the selector input[type=date], provide it as NG_VALUE_ACCESSOR with multi: true, and implement ControlValueAccessor. The directive's writeValue pushes the form control value into the native input, while registerOnChange stores the callback used to push user edits back into the control. Because the selector matches every date input, the conversion logic applies to all of them without extra markup or per-component wiring.

How do I convert an ISO date string to YYYY-MM-DD for an input type date in Angular?

Call formatDate from @angular/common inside writeValue to turn the ISO string from the form control into a YYYY-MM-dd string, then set it on the element with Renderer2.setAttribute(element, 'value', UIValue). The article passes 'en-IN' as the locale argument. This keeps the control holding a full ISO timestamp, such as one returned by an API, while the browser's date input receives the format it requires.

How do I convert the value of an input type date to an ISO string in an Angular form control?

Read the value with @HostListener('input', ['$event.target.valueAsNumber']), which returns milliseconds since the epoch rather than a string. Build a Date from that number and pass toISOString() to the callback given to registerOnChange. Helper functions getDate and isValidDate return a fallback whose toISOString() yields null for empty or invalid input, so the control value becomes null instead of a broken string.

How do I add validation to a custom ControlValueAccessor directive with NG_VALIDATORS in Angular?

Add NG_VALIDATORS to the directive's providers with useExisting pointing to the same class, then implement the Validator interface with a validate(control: AbstractControl) method. The article's version returns { date: true } when the control value is empty or does not produce a valid Date, and null otherwise. Angular runs validate whenever the control's value changes, so invalid dates are flagged automatically in any form that uses the directive.

How do I show a validation message only after the user interacts with a date field in Angular?

Show the error by combining touched or dirty with invalid in an *ngIf, such as (date?.touched || date?.dirty) && date?.invalid. Expose the control through a getter like get date() on the component to keep the template short. This avoids displaying the 'Invalid Date' message before the user has changed anything, while still reporting an invalid value after the user edits the field.

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.