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

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

Conversion of values

// src/app/directives/date-input.directive.tsimport { 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 {}
}
  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
  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.

Writing value to the native DOM element with writeValue

// src/app/directives/date-input.directive.tsimport { 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
);
}
}
  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
// 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');
}
}
  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>

Getting value from the native DOM element with registerOnChange

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

// ...
}
// 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());
};
}
}
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);
}

Validation

// 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,
},
],
})
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 };
}
}
<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>
*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

Conclusion

  • 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

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store