components icon indicating copy to clipboard operation
components copied to clipboard

datepicker: shows date using provided LOCALE_ID but parses date using default US locale (or a combination of them)

Open relair opened this issue 6 years ago • 22 comments
trafficstars

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

  1. Select 27/10/2018 using date picker.
  2. Modify the date by editing the input to 26/10/2018
  3. Observe invalid input

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.

relair avatar Nov 27 '18 15:11 relair

Have a look at the documentation https://material.angular.io/components/datepicker/overview

Especially the chapter "Choosing a date implementation and date format settings"

manklu avatar Nov 28 '18 22:11 manklu

Closing as this is a limitation of JavaScript's native parsing. I'd recommend using a different DateAdapter as suggested above

mmalerba avatar Dec 03 '18 18:12 mmalerba

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

relair avatar Dec 03 '18 19:12 relair

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

mmalerba avatar Dec 03 '18 21:12 mmalerba

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

relair avatar Dec 04 '18 11:12 relair

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:

  1. User already has locale setup in angular
  2. User installs material library and puts a date picker on his page without setting anything else, noticing it is using his locale format
  3. User finds a bug as although date picker displays format in his locale it doesn't parse the typed in date in the same format as expected.

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

relair avatar Dec 04 '18 12:12 relair

I'll reopen this as a docs issue

mmalerba avatar Dec 04 '18 18:12 mmalerba

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

  • Expected behavior is that manual input in DatePicker would be parsed using the local that was setup.
  • Understand the "Date" limitations but we should add a big red flag in documentation to avoid confusion.

Thanks. Keep up the good work.

Poseclop avatar Jul 05 '19 09:07 Poseclop

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

samwilliscreative avatar Jul 26 '19 09:07 samwilliscreative

@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]}
    ]
})

Solution found here Angular documentation here

Poseclop avatar Jul 26 '19 11:07 Poseclop

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

nicoabie avatar Apr 11 '20 20:04 nicoabie

Codecov Report

Merging #3846 (40a0bd4) into master (1cd6444) will decrease coverage by 0.02%. The diff coverage is n/a.

@@             Coverage Diff              @@
##             master    #3846      +/-   ##
============================================
- Coverage     64.54%   64.53%   -0.02%     
- Complexity     8171     8177       +6     
============================================
  Files          1147     1147              
  Lines         33795    33795              
  Branches       3014     3014              
============================================
- Hits          21813    21809       -4     
- Misses        10260    10265       +5     
+ Partials       1722     1721       -1     

see 3 files with indirect coverage changes

:mega: We’re building smart automated test selection to slash your CI/CD build times. Learn more

jeremylcarter avatar Jan 06 '21 03:01 jeremylcarter

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.

dcchristopher avatar Feb 09 '21 16:02 dcchristopher

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.

ajuanjojjj avatar Oct 15 '21 11:10 ajuanjojjj

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' },
  },
};

webdevelopland avatar Dec 23 '21 12:12 webdevelopland

Just use the MomentDateAdapter from @angular/material-moment-adapter and follow this answer on stackoverflow

ezhupa99 avatar Feb 18 '22 14:02 ezhupa99

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

azerafati avatar Sep 12 '22 11:09 azerafati

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

atonu avatar Sep 15 '22 13:09 atonu

Just use the MomentDateAdapter from @angular/material-moment-adapter and follow this answer on stackoverflow

This solution worked perfectly for me! Tks!

victorwvieira avatar Oct 07 '22 09:10 victorwvieira

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?

elegon32 avatar Mar 15 '24 21:03 elegon32

Same behavior as described by @elegon32. This is a very annoying bug.

WEBSosman avatar May 05 '24 17:05 WEBSosman

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?

mattiLeBlanc avatar May 08 '24 05:05 mattiLeBlanc

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

elegon32 avatar May 25 '24 17:05 elegon32

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 avatar May 26 '24 15:05 elegon32

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

mattiLeBlanc avatar May 27 '24 07:05 mattiLeBlanc