angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.23k stars 6.7k forks source link

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

Open relair opened 5 years ago

relair commented 5 years ago

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.

manklu commented 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"

mmalerba commented 5 years ago

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

relair commented 5 years ago

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

mmalerba commented 5 years ago

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

relair commented 5 years ago

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 commented 5 years ago

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

mmalerba commented 5 years ago

I'll reopen this as a docs issue

Poseclop commented 5 years ago

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

samwilliscreative commented 4 years ago

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

Poseclop commented 4 years ago

@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

nicoabie commented 4 years ago

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

jeremylcarter commented 3 years ago

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

dcchristopher commented 3 years ago

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.

ajuanjojjj commented 2 years ago

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.

webdevelopland commented 2 years ago

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' },
  },
};
ezhupa99 commented 2 years ago

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

azerafati commented 1 year ago

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

atonu commented 1 year ago

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.
    ],
victorwvieira commented 1 year ago

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

This solution worked perfectly for me! Tks!

elegon32 commented 4 months ago

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?

WEBSosman commented 2 months ago

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

mattiLeBlanc commented 2 months ago

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?

elegon32 commented 1 month ago

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 commented 1 month ago

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.

mattiLeBlanc commented 1 month ago

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