Angular 2: How to use JavaScript Date Object with NgModel two way binding
I'm working with Angular 2 and I have this code:
JS, this code initiates the employee-variable for the template:
handleEmployee(employee : Employee){
this.employee = employee;
this.employee.startDate = new Date('2005/01/01');
console.log(this.employee);
}
Template:
...
<div>
<label>Start date: </label>
<input [(ngModel)]="employee.startDate" type="date" name="startDate"/>
</div>
<div>
...
Other data like firstname is displayed correctly. But for the date I just get:
mm/dd/yyyy
In the input element, which should be a date.
How can I do this?
UPDATE:
StackBlitz
when I wrote this answer DatePipe did not exist, now you can just do this
<input [ngModel]="startDate | date:'yyyy-MM-dd'" (ngModelChange)="startDate = $event" type="date" name="startDate"/>
`
Old Answer:
PLUNKER
You need to convert date object
in the input type="date"
format which is yyyy-mm-dd
, this is how it will work
Template:
<input [(ngModel)]="humanDate" type="date" name="startDate"/>
Component (TS):
export class App {
startDate: any;
constructor() {
this.startDate = new Date(2005, 1, 4);
}
set humanDate(e){
e = e.split('-');
let d = new Date(Date.UTC(e[0], e[1]-1, e[2]));
this.startDate.setFullYear(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
}
get humanDate(){
return this.startDate.toISOString().substring(0, 10);
}
}
Read pipes and ngModel and my deсision:
<input type="date" class="form-control" id="myDate" [ngModel]="myDate | date:'y-MM-dd'" (ngModelChange)="myDate = $event" name="birthday">
FormControls (both template-driven and reactive) subscribe for values and write values via Directives that implement ControlValueAccessor
. Take a look at the relevant method selectValueAccessor, which is used in all necessary directives. Normal input controls (e.g. <input type="text">
) or textareas are handled by the DefaultValueAccessor. Another example is the CheckboxValueAccessor which is applied to checkbox input controls.
The job isn't complicated at all. We just need to implement a new value accessor for date input controls.DateValueAccessor
is a nice name:
// date-value-accessor.ts
import { Directive, ElementRef, HostListener, Renderer, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
export const DATE_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DateValueAccessor),
multi: true
};
/**
* The accessor for writing a value and listening to changes on a date input element
*
* ### Example
* `<input type="date" name="myBirthday" ngModel useValueAsDate>`
*/
@Directive({
selector: '[useValueAsDate]',
providers: [DATE_VALUE_ACCESSOR]
})
export class DateValueAccessor implements ControlValueAccessor {
@HostListener('input', ['$event.target.valueAsDate']) onChange = (_: any) => { };
@HostListener('blur', []) onTouched = () => { };
constructor(private _renderer: Renderer, private _elementRef: ElementRef) { }
writeValue(value: Date): void {
this._renderer.setElementProperty(this._elementRef.nativeElement, 'valueAsDate', value);
}
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
setDisabledState(isDisabled: boolean): void {
this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
}
}
We attach the DateValueAccessor
to the multi-provider DATE_VALUE_ACCESSOR
, so that selectValueAccessor can find it.
The only question is, which selector should be used. I decided for an opt-in solution.
Here the DateValueAccessor selects on the attribute "useValueAsDate".
<input type="date" name="myBirthday" ngModel useValueAsDate>
OR
<input type="date" name="myBirthday" [(ngModel)]="myBirthday" useValueAsDate>
OR
<input type="date" formControlName="myBirthday" useValueAsDate>
It is also possible to fix the default implementation.
The following selector would activate the feature magically.
// this selector changes the previous behavior silently and might break existing code
selector: 'input[type=date][formControlName],input[type=date][formControl],input[type=date][ngModel]'
But please be aware, that this might break existing implementations that rely of the old behaviour. So I would go for the opt-in version!
It's all on NPM and Github
For your convenience, I created the project angular-data-value-accessor
on Github.
There is also a NPM package available:
npm install --save angular-date-value-accessor
Then just import the module via NgModule:
// app.module.ts
import { DateValueAccessorModule } from 'angular-date-value-accessor';
@NgModule({
imports: [
DateValueAccessorModule
]
})
export class AppModule { }
Now you can apply the "useValueAsDate" to your date input controls.
Demo
Of course, there is a demo at: http://johanneshoppe.github.io/angular-date-value-accessor/
I began trying to implement Ankit Singh's solution and ran in to a few problems with validation and timezone stuff. (Even after trying the suggestions in the comment section of that answer)
Instead I chose to utilize moment.js to handle the transforming between string and date using ISO8601 format date strings. I've had great results in the past using moment.js so this was not a difficult decision. Seems to be working well for me, hopefully someone else finds this useful.
For my Angular 2 app I ran npm install --save moment and then turned Ankit's solution into a wrapper around a js Date object:
import * as moment from 'moment';
export class NgDate {
date: any;
constructor() {
this.date = new Date();
}
set dateInput(e) {
if (e != "") {
var momentDate = moment(e, moment.ISO_8601).toDate();
this.date = momentDate;
}
else {
this.date = null;
}
}
get dateInput() {
if(this.date == null)
{
return "";
}
var stringToReturn = moment(this.date).format().substring(0, 10);
return stringToReturn;
}
}
Then for the HTML:
<input type="date" name="someDate" [(ngModel)]="ngDateModel.dateInput"/>
Fixed it with this code:
handleEmployee(employee : Employee){
this.employee = employee;
let dateString : string = employee.startDate.toString();
let days : number = parseInt(dateString.substring(8, 10));
let months : number = parseInt(dateString.substring(5, 7));
let years : number = parseInt(dateString.substring(0, 5));
let goodDate : Date = new Date(years + "/" + months + "/" + days);
goodDate.setDate(goodDate.getDate() + 2);
this.date = goodDate.toISOString().substring(0, 10);
}
Html:
<div>
<label>Start date: </label>
<input [(ngModel)]="date" type="date" name="startDate"/>
</div>