Skip to content

Instantly share code, notes, and snippets.

@julianwachholz
Created January 3, 2020 12:22
Show Gist options
  • Save julianwachholz/abec7b951508cce1b1e621b6afb65f70 to your computer and use it in GitHub Desktop.
Save julianwachholz/abec7b951508cce1b1e621b6afb65f70 to your computer and use it in GitHub Desktop.
Ant Design <DatePicker /> with native Date and date-fns

Native Date DatePicker component for Ant.Design

Use just like the regular DatePicker. Currently only uses a fixed locale.

import generatePicker from "antd/lib/date-picker/generatePicker";
import {
addDays,
addMonths,
addYears,
format,
getWeek,
isAfter,
isValid,
parse,
setDate,
setHours,
setMinutes,
setMonth,
setSeconds,
setYear
} from "date-fns";
import { enUS } from "date-fns/locale";
import { GenerateConfig } from "rc-picker/lib/generate";
const generateConfig: GenerateConfig<Date> = {
getNow: () => new Date(),
getWeekDay: date => date.getDay(),
getYear: date => date.getFullYear(),
getMonth: date => date.getMonth(),
getDate: date => date.getDate(),
getHour: date => date.getHours(),
getMinute: date => date.getMinutes(),
getSecond: date => date.getSeconds(),
addYear: (date, diff) => addYears(date, diff),
addMonth: (date, diff) => addMonths(date, diff),
addDate: (date, diff) => addDays(date, diff),
setYear: (date, year) => setYear(date, year),
setMonth: (date, month) => setMonth(date, month),
setDate: (date, num) => setDate(date, num),
setHour: (date, hour) => setHours(date, hour),
setMinute: (date, minute) => setMinutes(date, minute),
setSecond: (date, second) => setSeconds(date, second),
isAfter: (date1, date2) => isAfter(date1, date2),
isValidate: date => isValid(date),
locale: {
getWeekFirstDay: locale => 1,
getWeek: (locale, date) => getWeek(date),
getShortWeekDays: locale => {
const d = Array.from({ length: 7 }).map((_, day) => {
console.info("localize day ", day);
return enUS.localize.day(day, { width: "abbreviated" });
});
console.log("days", d);
return d;
},
getShortMonths: locale => {
const m = Array.from({ length: 12 }).map((_, month) =>
enUS.localize.month(month, { width: "abbreviated" })
);
console.log("months", m);
return m;
},
format: (locale, date, fmt) => {
fmt = fmt.replace("YYYY", "yyyy");
fmt = fmt.replace("DD", "dd");
return format(date, fmt, { locale: enUS });
},
parse: (locale, text, formats) => {
const fmt = formats[0].toLowerCase();
return parse(text, fmt, new Date());
}
}
};
const DatePicker = generatePicker<Date>(generateConfig);
export default DatePicker;
@kbzowski
Copy link

Updated version for [email protected]

import generatePicker from "antd/lib/date-picker/generatePicker";
import {
  addDays,
  addMonths,
  addYears,
  format,
  getWeek,
  isAfter,
  isValid,
  lastDayOfMonth,
  parse,
  setDate,
  setHours,
  setMinutes,
  setMonth,
  setSeconds,
  setYear,
} from "date-fns";
import { pl } from "date-fns/locale";
import { GenerateConfig } from "rc-picker/lib/generate";

const generateConfig: GenerateConfig<Date> = {
  getNow: () => new Date(),
  getWeekDay: (date) => date.getDay(),
  getYear: (date) => date.getFullYear(),
  getMonth: (date) => date.getMonth(),
  getDate: (date) => date.getDate(),
  getHour: (date) => date.getHours(),
  getMinute: (date) => date.getMinutes(),
  getSecond: (date) => date.getSeconds(),

  addYear: (date, diff) => addYears(date, diff),
  addMonth: (date, diff) => addMonths(date, diff),
  addDate: (date, diff) => addDays(date, diff),
  setYear: (date, year) => setYear(date, year),
  setMonth: (date, month) => setMonth(date, month),
  setDate: (date, num) => setDate(date, num),
  setHour: (date, hour) => setHours(date, hour),
  setMinute: (date, minute) => setMinutes(date, minute),
  setSecond: (date, second) => setSeconds(date, second),

  isAfter: (date1, date2) => isAfter(date1, date2),
  isValidate: (date) => isValid(date),

  getFixedDate: (fixed: string) => parse(fixed, "yyyy-MM-dd", new Date()),
  getEndDate: (value: Date) => lastDayOfMonth(value),

  locale: {
    getWeekFirstDay: (locale) => 1,
    getWeekFirstDate: (locale) => new Date(),
    getWeek: (locale, date) => getWeek(date),
    getShortWeekDays: (locale) => {
      return Array.from({ length: 7 }).map((_, day) => {
        return pl.localize!.day(day, { width: "abbreviated" });
      });
    },
    getShortMonths: (locale) => {
      return Array.from({ length: 12 }).map((_, month) =>
        pl.localize!.month(month, { width: "abbreviated" })
      );
    },
    format: (locale, date, fmt) => {
      fmt = fmt.replace("YYYY", "yyyy");
      fmt = fmt.replace("DD", "dd");
      return format(date, fmt, { locale: pl });
    },
    parse: (locale, text, formats) => {
      const fmt = formats[0].toLowerCase();
      return parse(text, fmt, new Date());
    },
  },
};
export const DatePicker = generatePicker<Date>(generateConfig);

@thomastvedt
Copy link

Hey, thanks for this 🙏 Did you ever make one with locale switchable on runtime?

Is there no way to pass locale dynamically to ant.design DatePicker on runtime?

import dateFnsGenerateConfig from 'rc-picker/lib/generate/dateFns';
import generatePicker, {
  PickerProps,
} from 'antd/es/date-picker/generatePicker';
import 'antd/es/date-picker/style/index';
import {
  format,
  getWeek, isValid,
  Locale as DateFnsLocale,
  parse
} from 'date-fns';
import { GenerateConfig } from 'rc-picker/lib/generate';
import React from 'react';
import { dateFnsLocaleDict } from 'i18n/dateFnsLocale';
import useLocale from 'i18n/useLocale';

// NOTE: copy-paste from dateFnsGenerateConfig
function localeParse(format: string) {
  return format
    .replace(/Y/g, 'y')
    .replace(/D/g, 'd')
    .replace(/gggg/, 'yyyy')
    .replace(/g/g, 'G')
    .replace(/([Ww])o/g, 'wo');
}

// get localized config
const getLocalizedGenerateConfig = (
  dateFnsLocale: DateFnsLocale
): GenerateConfig<Date> => {
  return {
    ...dateFnsGenerateConfig, // date-fns defaults defined by ant.design as a starting point
    getFixedDate: (fixed: string) => parse(fixed, 'yyyy-MM-dd', new Date()),
    locale: {
      getWeekFirstDay: (locale) => dateFnsLocale.options?.weekStartsOn || 1,
      getWeekFirstDate: (locale) => new Date(),
      getWeek: (locale, date) => getWeek(date),
      getShortWeekDays: (locale) => {
        return Array.from({ length: 7 }).map((_, day) => {
          return dateFnsLocale.localize!.day(day, { width: 'short' });
        });
      },
      getShortMonths: (locale) => {
        return Array.from({ length: 12 }).map((_, month) =>
          dateFnsLocale.localize!.month(month, { width: 'abbreviated' })
        );
      },
      format: (locale, date, fmt) => {
        fmt = fmt.replace('YYYY', 'yyyy');
        fmt = fmt.replace('DD', 'dd');
        return format(date, fmt, { locale: dateFnsLocale });
      },
      parse: (locale, text, formats) => {
        for (let i = 0; i < formats.length; i += 1) {
          const format = localeParse(formats[i]);
          const date = parse(text, format, new Date(), {
            locale: dateFnsLocale,
            weekStartsOn: dateFnsLocale.options?.weekStartsOn
          });

          if (isValid(date)) {
            return date;
          }
        }
        return null;
      }
    },
  };
};

const FnsDatePickerDa = generatePicker<Date>(
  getLocalizedGenerateConfig(dateFnsLocaleDict['da'].locale)
);
const FnsDatePickerDe = generatePicker<Date>(
  getLocalizedGenerateConfig(dateFnsLocaleDict['de'].locale)
);
const FnsDatePickerEt = generatePicker<Date>(
  getLocalizedGenerateConfig(dateFnsLocaleDict['et'].locale)
);
const FnsDatePickerEn = generatePicker<Date>(
  getLocalizedGenerateConfig(dateFnsLocaleDict['en'].locale)
);
const FnsDatePickerFr = generatePicker<Date>(
  getLocalizedGenerateConfig(dateFnsLocaleDict['fr'].locale)
);
const FnsDatePickerNl = generatePicker<Date>(
  getLocalizedGenerateConfig(dateFnsLocaleDict['nl'].locale)
);
const FnsDatePickerNo = generatePicker<Date>(
  getLocalizedGenerateConfig(dateFnsLocaleDict['no'].locale)
);
const FnsDatePickerFi = generatePicker<Date>(
  getLocalizedGenerateConfig(dateFnsLocaleDict['fi'].locale)
);
const FnsDatePickerSv = generatePicker<Date>(
  getLocalizedGenerateConfig(dateFnsLocaleDict['sv'].locale)
);

type DateFnsTypedPickerProps = PickerProps<Date>;

/***
 * Based on https://ant.design/docs/react/replace-moment#Use-date-fns
 * And https://gist.github.com/julianwachholz/abec7b951508cce1b1e621b6afb65f70
 * I don't think we can dynamically pass locale to ant.d DatePicker??
 * Is there a better way to do this?
 */
const DatePicker: React.FC<DateFnsTypedPickerProps> = (props) => {
  const locale = useLocale();

  switch (locale.localeKey) {
    case 'de':
      return <FnsDatePickerDe {...props} />;
    case 'da':
      return <FnsDatePickerDa {...props} />;
    case 'en':
      return <FnsDatePickerEn {...props} />;
    case 'et':
      return <FnsDatePickerEt {...props} />;
    case 'fr':
      return <FnsDatePickerFr {...props} />;
    case 'fi':
      return <FnsDatePickerFi {...props} />;
    case 'nl':
      return <FnsDatePickerNl {...props} />;
    case 'no':
      return <FnsDatePickerNo {...props} />;
    case 'sv':
      return <FnsDatePickerSv {...props} />;
  }
};

export default DatePicker;

dateFnsLocale.ts:

import { Locale as DateFnsLocale } from 'date-fns';
import { da, de, enUS, et, fi, fr, nb, nl, sv } from 'date-fns/locale';

export const dateFnsLocaleDict: {
  [code: string]: {
    locale: DateFnsLocale;
    key: 'da' | 'de' | 'ed' | 'enUS' | 'fr' | 'nl' | 'nb' | 'fi' | 'sv';
  };
} = {
  da: { locale: da, key: 'da' },
  de: { locale: de, key: 'de' },
  et: { locale: et, key: 'ed' },
  en: { locale: enUS, key: 'enUS' },
  fr: { locale: fr, key: 'fr' },
  nl: { locale: nl, key: 'nl' },
  no: { locale: nb, key: 'nb' },
  fi: { locale: fi, key: 'fi' },
  sv: { locale: sv, key: 'sv' },
};

// Used in webpack config to reduce bundle size:
// https://date-fns.org/v2.24.0/docs/webpack
export const supportedDateFnsLocales = Object.values(dateFnsLocaleDict).map(
  (c) => c.key
);

@kbzowski
Copy link

kbzowski commented Feb 9, 2022

There are switching to DayJS with antd v5 - maybe that will solve all the problems.
ant-design/ant-design#33862

@kbzowski
Copy link

kbzowski commented Feb 10, 2022

@thomastvedt For some reason your solution made my bundle grow by nearly 1MB even though I only added 2 languages.
It seems that dateFnsGenerateConfig is to blame.

@asyncink
Copy link

asyncink commented Jan 21, 2023

The solutions above have a serious drawback - when entering an invalid date (for example, if you type a space in input field), the DatePicker crashes with the entire app.

We can fix this by referencing antd implementation: https://github.com/react-component/picker/blob/master/src/generate/dateFns.ts.

We can also modify the locale field so that the picker explicitly works with a specific locale:

import generatePicker from 'antd/lib/date-picker/generatePicker'

import type { GenerateConfig } from 'rc-picker/lib/generate'
import config from 'rc-picker/lib/generate/dateFns'

import {
  isValid,
  getWeek,
  startOfWeek,
  format as formatDate,
  parse as parseDate
} from 'date-fns'
import { ru } from 'date-fns/locale'

const localeParse = (format: string) => {
  return format
    .replace(/Y/g, 'y')
    .replace(/D/g, 'd')
    .replace(/gggg/, 'yyyy')
    .replace(/g/g, 'G')
    .replace(/([Ww])o/g, 'wo')
}

const locale: GenerateConfig<Date>['locale'] = {
  getWeekFirstDay: () => {
    const clone = ru

    return clone.options?.weekStartsOn ?? 1
  },
  getWeekFirstDate: (locale, date) => {
    return startOfWeek(date, { locale: ru })
  },
  getWeek: (locale, date) => {
    return getWeek(date, { locale: ru })
  },
  getShortWeekDays: () => {
    const clone = ru

    return Array.from({ length: 7 }).map((_, i) =>
      clone.localize?.day(i, { width: 'short' })
    )
  },
  getShortMonths: () => {
    const clone = ru

    return Array.from({ length: 12 }).map((_, i) =>
      clone.localize?.month(i, { width: 'abbreviated' })
    )
  },
  format: (locale, date, format) => {
    if (!isValid(date)) {
      /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
      return null as any
    }

    return formatDate(date, localeParse(format), {
      locale: ru
    })
  },
  parse: (locale, text, formats) => {
    for (let i = 0; i < formats.length; i += 1) {
      const format = localeParse(formats[i])
      const formatText = text
      const date = parseDate(formatText, format, new Date(), {
        locale: ru
      })

      if (isValid(date)) {
        return date
      }
    }

    return null
  }
}

export const DatePicker = generatePicker<Date>({ ...config, locale })

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment