Open relair opened 5 years ago
Have a look at the documentation https://material.angular.io/components/datepicker/overview
Especially the chapter "Choosing a date implementation and date format settings"
Closing as this is a limitation of JavaScript's native parsing. I'd recommend using a different DateAdapter as suggested above
Why use native parsing by default if its so bad? So I need to use a workaround to make it working properly as it wouldn't work on different cultures by default? Why is datepicker picking culture settings knowing that it wouldn't work for them properly anyway? So you just close the obvious bugs just because there is a workaround and you can't be bothered to fix them? If I known that I wouldn't start using this library in the first place...
People have different needs for different apps, we don't want to mandate a heavy-weight dependency for people that don't need it. We provide both a NativeDateAdapter and a MomentDateAdapter and people are free to write additional adapters if that's what they need for their use case
I don't understand why would you use locale for displaying date but not for parsing, so you knowingly make it format date using one format and parse it using the other format? Would make more sense to have native provider not pick up locale settings at all if you cannot make it work both ways.
For parsing you only have new Date(Date.parse(value))
which understandably requires date in specific format which may or may not be consistent with Locale settings (most of the time it is not, unless you use US locale)
My suggestion is rather than obscuring the parsing issue make it apparent that NativeDateAdapter doesn't support locales and get rid of it for formatting dates, as you cannot make it work without overhead of making sure you provide the right format to Date.parse (which is pretty much what moment covers for you).
I saw the note in the documentation
Please note: MatNativeDateModule is based off of the functionality available in JavaScript's native Date object, and is thus not suitable for many locales.
At the point how it works right now it should be in bold red text at the top of the page, not hidden among some details. Look at it from perspective of a developer(user) using your library:
I think it should work consistently 'out of the box' (as nothing was setup for it specifically - even the locale wasn't set at this point as MAT_DATE_LOCALE just inherited generic one). Then you can do some setup, for example to have custom date format you may want to look into different date adapters and some detailed configuration documentation. Right now it looks like when you just happen to have a locale setup it won't work consistently by default, and that is what this bug is about. So instead of
Date picker uses provided en-GB LOCALE_ID for parsing typed in date - 27/10/2018 input manually by typing is valid
You can make it
Date picker has consistent date format between parsing and formatting
I'll reopen this as a docs issue
+1 on that one as the current behavior is unexpected and ended up as a bug in our dev branch (highly critical as it does not throw any exception yet potentially has our customers looks at the wrong data).
Thanks. Keep up the good work.
@relair @Poseclop Did you manage to get the typed date parsing correctly? I'm still unable to get it working even when adding the MAT_DATE_LOCALE fixes.
@swillisstudio Hi Sam.
MAT_DATE_LOCALE
will format the data before displaying it in the component but it does not alter the parsing of dates entered manually (for that it will always use the US default format: MM.DD.YYYY).
The solution is to create a custom DateAdapter
class then provide it instead of the default Native Adapter
See below a very simple exemple of Date parser:
export class CustomDateAdapter extends NativeDateAdapter {
parse(value: any): Date | null {
const currentDate = new Date();
let year: number = currentDate.getFullYear();
let month: number = currentDate.getMonth();
let day: number = currentDate.getDate();
if ((typeof value === 'string') &&
((value.indexOf('/') > -1) || (value.indexOf('.') > -1) || (value.indexOf('-') > -1))) {
const str = value.split(/[\./-]/);
day = !!str[0] ? +str[0] : day;
month = !!str[1] ? +str[1] - 1 : month;
year = !!str[2] ?
// If year is less than 3 digit long, we add 2000.
+str[2].length <= 3 ? +str[2] + 2000 : +str[2] : year ;
return new Date(year, month, day);
}
}
}
..then adding it to Material module:
@NgModule({
exports: [
MatDatepickerModule,
MatNativeDateModule,
...
],
providers: [
{provide: DateAdapter, useClass: CustomDateAdapter, deps: [MAT_DATE_LOCALE, Platform]}
]
})
I post a solution I had to implement at a rush during covid19 using date-fns
import { NativeDateAdapter } from '@angular/material/core';
import { parse, format as dateFnsFormat } from 'date-fns';
export class CustomDateAdapter extends NativeDateAdapter {
readonly DT_FORMAT = 'dd/MM/yyyy';
parse(value: string | null): Date | null {
if (value) {
value = value.trim();
if(!value.match(/^\d{1,2}\/\d{1,2}\/\d{4}$/)) {
return new Date(NaN);
}
return parse(value, this.DT_FORMAT, new Date())
}
return null;
}
format(date: Date, displayFormat: Object): string {
return dateFnsFormat(date, this.DT_FORMAT)
}
}
Then included provider as @Poseclop did. Thanks @Poseclop
I'm sure there is a better way to do this but the docs are lacking some information on how to do it right
I post a solution I had to implement at a rush during covid19 using date-fns
import { NativeDateAdapter } from '@angular/material/core'; import { parse, format as dateFnsFormat } from 'date-fns'; export class CustomDateAdapter extends NativeDateAdapter { readonly DT_FORMAT = 'dd/MM/yyyy'; parse(value: string | null): Date | null { if (value) { value = value.trim(); if(!value.match(/^\d{1,2}\/\d{1,2}\/\d{4}$/)) { return new Date(NaN); } return parse(value, this.DT_FORMAT, new Date()) } return null; } format(date: Date, displayFormat: Object): string { return dateFnsFormat(date, this.DT_FORMAT) } }
Then included provider as @Poseclop did. Thanks @Poseclop
I'm sure there is a better way to do this but the docs are lacking some information on how to do it right
Thanks for a quick solution. I see there is some discussion about getting adapters for Luxon and date-fns here https://github.com/angular/components/pull/14681
Thank you, @relair, because your LOCALE_ID provider suggestion saved me a lot of headaches when debugging Angular code. Chrome on MacOS sets the default locale to 'en-US' which causes moment to throw an exception (which interrupts my debugging) on every mention of a moment date adapter (everywhere a date picker is used, etc.). By setting the LOCALE_ID to 'en', those errors go away.
I agree with you that the parsing needs to be as localized as the display.
export class CustomDateAdapter extends NativeDateAdapter { parse(value: any): Date | null { const currentDate = new Date(); let year: number = currentDate.getFullYear(); let month: number = currentDate.getMonth(); let day: number = currentDate.getDate();
if ((typeof value === 'string') && ((value.indexOf('/') > -1) || (value.indexOf('.') > -1) || (value.indexOf('-') > -1)) { const str = value.split(/[\./-]/); day = !!str[0] ? +str[0] : day; month = !!str[1] ? +str[1] - 1 : month; year = !!str[2] ? // If year is less than 3 digit long, we add 2000. +str[2].length <= 3 ? +str[2] + 2000 : +str[2] : year ; return new Date(year, month, day); } }
}
Missing a )
on line 9, closing the if
statement
Also, the current behaviour where the date could be correctly displayed, but if a user changes say, the year, months and days are interchanged is problematic at best. If it can't work properly, other solutions should be explored, since any implementation done before the docs are updated, and any implementation done after checking an example somewhere else (And there are lots of examples elsewhere) will silently fail until someone reports the wrong behaviour to the dev team.
Its not even something obvious or easy to catch, since most people will use the popup selector instead of text input.
My solution, in case someone would need
export class AppDateAdapter extends NativeDateAdapter {
parse(value: string): Date | null {
// 15.07.18 -> [15, 7, 18]
const values: string[] = value.replace(/[\.\\-]/g, '/').split('/');
const date: number = Number(values[0]);
const month: number = Number(values[1]) - 1;
let year: number = Number(values[2]);
if (!year) {
// If year not set then the year is current year
// e.g. 05/07 = 05/07/19
year = (new Date(Date.now())).getUTCFullYear();
} else if (year < 1000) {
// Without the fix 02 = 1902
year += 2000; // Welcome to 21 century
}
// Invalid Date fix
let parsedDate: Date = new Date(year, month, date);
if (
isNaN(parsedDate.getTime()) ||
date > 31 ||
month > 11
) {
parsedDate = null;
}
return parsedDate;
}
format(date: Date, displayFormat: string): string {
if (displayFormat === 'input') {
const day: number = date.getDate();
const month: number = date.getMonth() + 1;
const year: number = date.getFullYear();
return this.to2digit(day) + '/' + this.to2digit(month) + '/' + year;
} else if (displayFormat === 'inputMonth') {
const month: number = date.getMonth() + 1;
const year: number = date.getFullYear();
return this.to2digit(month) + '/' + year;
} else {
return date.toDateString();
}
}
private to2digit(n: number): string {
return ('00' + n).slice(-2);
}
}
export const APP_DATE_FORMATS = {
parse: {
dateInput: { month: 'short', year: 'numeric', day: 'numeric' },
},
display: {
dateInput: 'input',
monthYearLabel: 'inputMonth',
dateA11yLabel: { year: 'numeric', month: 'long', day: 'numeric' },
monthYearA11yLabel: { year: 'numeric', month: 'long' },
},
};
Just use the MomentDateAdapter from @angular/material-moment-adapter and follow this answer on stackoverflow
What I would suggest is a warning in the console or during build that checks LOCALE_ID
and DateAdapter
and if it's something other than en-US
but still using NativeDateAdapter
, it would warn this issue and maybe link back here
MomentDateAdapter v8.1.4 is working fine with my project running angular v12.2.0 However I was unable to use MomentDateAdapter versions ranging from 12.2 to 13.2. V8.1.4 seems to be working fine
heres the import array I used:
import { LOCALE_ID} from '@angular/core';
import {
DateAdapter,
MAT_DATE_FORMATS,
MAT_DATE_LOCALE,
} from '@angular/material/core';
import { MomentDateAdapter } from '@angular/material-moment-adapter';
export const DATE_FORMATS = {
parse: {
dateInput: 'DD.MM.YYYY',
},
display: {
dateInput: 'DD.MM.YYYY',
monthYearLabel: 'MMM YYYY',
dateA11yLabel: 'LL',
monthYearA11yLabel: 'MMMM YYYY',
},
};
providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
{ provide: MAT_DATE_FORMATS, useValue: DATE_FORMATS },
{ provide: LOCALE_ID, useValue: 'de-DE' }, // for German translation. Ignore this if not needed.
],
Just use the MomentDateAdapter from @angular/material-moment-adapter and follow this answer on stackoverflow
This solution worked perfectly for me! Tks!
As of today, with Angular 17, I am not able to produce a consistent UX with Datepicker in German. Here is my stackbliz: https://stackblitz.com/edit/stackblitz-starters-3t5zt8?file=src%2Fapp%2Fparent%2Fparent.component.ts
Just select 3.3.2024 via the picker. Then, in input field, prepend a 1 -> 13.3.2024 -> We get an error - which is wrong! Then, replace the 3 with 0 -> 10.3.2024 -> then TAB out of the input filed -> Date gets transformed into 3.10.204 - which is wrong!
I already tried so many things, including using the CustomDateAdapter seen in this thread here and also this: https://stackoverflow.com/a/77097002 - which didn't work either, because the date comes into the 'onDateInputed' function already parsed wrong -> 3.10.204 instead of 10.3.2024
This is sooo anoying... What am I doing wrong? Is it really not intended to work outside of a MM/DD/YYYY zone?
Same behavior as described by @elegon32. This is a very annoying bug.
I just wasted 5 hours on trying to work around having to use the Moment Datepicker adapter, which is brining unnecessary file size to my deployment. I also already use Dayjs which is much smaller.
My issue is that when I type a date, it turns 10/1/1940 (10 Jan 1940) into 1 October 1940. When I use the date picker itself it actually selects the date correctly.
Then I tried to create my own adapter, but I kept having the issue that even if the parse
function of my adapter returns a proper date, the date input field would still convert it to the American date.
Sorry for sounding rude or harsh, but I have bigger fish to fry then to have to deal with something like this. I dont understand why there is not a simple option saying, use this Adapter we created (we people that know how ths stuff works ) and you can pass your local and your custom formats etc, and it just works as exepcted.
The Material library is fantastic and I love using it. But dealing with these dates is just crazy. I am pretty sure the majority of the world doesnt use the American format.
Angular team, can you come up with a proper solution for this?
Is anyone of the google team aware of this? I really love angular and material is not that bad, but this here is really frustrating! Help!!!
Ok, digged into it once more. The Material Documentation contains some advice that helped me. This is a working example: https://stackblitz.com/edit/zp3skx?file=src%2Fexample%2Fdatepicker-formats-example.ts This is the point in the docs, that showed me the way: https://material.angular.io/components/datepicker/overview#customizing-the-parse-and-display-formats
After integration in my app, it now seems to work fine.
@elegon32 The problem is if you don't want to use Moment it seems to be more tricky. I give up building my own adapter because it was to distracting from what I wanted to work on and the input would still convert the date to US Date. I think MomentJS is a pretty big library I believe, specially compare to dayjs.
Bug, feature request, or proposal:
Bug. I don't think it is working like that on purpose.
What is the expected behavior?
Date picker uses provided en-GB LOCALE_ID for parsing typed in date - 27/10/2018 input manually by typing is valid
What is the current behavior?
Typed date is parsed using US locale - 27/10/2018 input manually is invalid, 12/01/2018 is December, not January.
What are the steps to reproduce?
https://stackblitz.com/edit/angular-datepicker-locale?file=main.ts
What is the use-case or motivation for changing an existing behavior?
Make material datepicker great again! But seriously: it seems like if a culture is provided it should affect both parsing and formatting aspects of dealing with dates and not format it into a date which is considered invalid when updated by typing.
Which versions of Angular, Material, OS, TypeScript, browsers are affected?
As in demo: Angular 7.1.0 Material 7.1.0. It fails validation on chrome if day (month?) is over 12, but on IE 11 is more curious as it modifies the year, I am fairly sure the swap issue causes it to work incorrectly on any browser.
Is there anything else we should know?
I think it may have been an issue with parsing the text date from text - it seems like it would parse using US culture ignoring provided locale.