Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Created useDateField #297

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/red-flies-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@reactive-forms/x': patch
---

Created useDateField hook
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"name": "Debug Core Tests",
"type": "node",
"request": "launch",
"runtimeArgs": ["--inspect-brk", "${workspaceFolder}/node_modules/aqu/dist/aqu.js", "test", "--runInBand"],
"cwd": "${workspaceFolder}/packages/core",
"runtimeArgs": ["--inspect-brk", ".\\node_modules\\jest\\bin\\jest.js", "--watch", "useDateField"],
"cwd": "${workspaceFolder}/packages/x",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
Expand Down
1 change: 1 addition & 0 deletions packages/x/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
],
"source": "src/index.ts",
"dependencies": {
"dayjs": "^1.11.9",
"lodash": "4.17.21"
}
}
30 changes: 30 additions & 0 deletions packages/x/src/DateFieldI18n.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { createContext, PropsWithChildren } from 'react';
import merge from 'lodash/merge';

import { formatDate } from './formatDate';

export type DateFieldI18n = {
required: string;
invalidInput: string;
minDate: (min: Date, pickTime: boolean) => string;
maxDate: (max: Date, pickTime: boolean) => string;
};

export const defaultDateFieldI18n: DateFieldI18n = {
required: 'Field is required',
invalidInput: 'Must be date',
minDate: (min, pickTime) => `Date must not be earlier than ${formatDate(min, pickTime)}`,
maxDate: (max, pickTime) => `Date must not be later than ${formatDate(max, pickTime)}`,
};

export const DateFieldI18nContext = createContext<DateFieldI18n>(defaultDateFieldI18n);

export type DateFieldI18nContextProviderProps = PropsWithChildren<{ i18n?: Partial<DateFieldI18n> }>;

export const DateFieldI18nContextProvider = ({ i18n, children }: DateFieldI18nContextProviderProps) => {
return (
<DateFieldI18nContext.Provider value={merge(defaultDateFieldI18n, i18n)}>
{children}
</DateFieldI18nContext.Provider>
);
};
9 changes: 9 additions & 0 deletions packages/x/src/formatDate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import dayjs from 'dayjs';

export const formatDate = (value: Date | null | undefined, pickTime: boolean) => {
if (!(value instanceof Date)) {
return '';
}

return dayjs(value).format(`YYYY.MM.DD${pickTime ? ' HH:mm' : ''}`);
};
6 changes: 4 additions & 2 deletions packages/x/src/useConverterField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ export class ConversionError extends Error {
}
}

export type ConverterFieldConfig<T> = {
export type ValueConverter<T> = {
parse: (value: string) => T;
format: (value: T) => string;
} & FieldConfig<T>;
};

export type ConverterFieldConfig<T> = ValueConverter<T> & FieldConfig<T>;

export type ConverterFieldBag<T> = {
text: string;
Expand Down
110 changes: 110 additions & 0 deletions packages/x/src/useDateField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { useCallback, useContext } from 'react';
import { FieldConfig, useFieldValidator } from '@reactive-forms/core';
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';

import { DateFieldI18nContext } from './DateFieldI18n';
import { formatDate } from './formatDate';
import { ConversionError, ConverterFieldBag, useConverterField, ValueConverter } from './useConverterField';

dayjs.extend(customParseFormat);

const defaultDateFormats = ['DD.MM.YYYY', 'YYYY-MM-DD', 'YYYY/MM/DD', 'YYYY.MM.DD', 'DD-MM-YYYY', 'DD/MM/YYYY'];
const defaultDateTimeFormats = [
'DD.MM.YYYY HH:mm',
'YYYY-MM-DD HH:mm',
'YYYY/MM/DD HH:mm',
'YYYY.MM.DD HH:mm',
'DD-MM-YYYY HH:mm',
'DD/MM/YYYY HH:mm',
];

export type DateFieldConfig = FieldConfig<Date | null | undefined> & {
required?: boolean;
minDate?: Date;
maxDate?: Date;
pickTime?: boolean;
} & Partial<ValueConverter<Date | null | undefined>>;

export type DateFieldBag = ConverterFieldBag<Date | null | undefined>;

export const useDateField = ({
name,
validator,
schema,
required,
minDate,
maxDate,
pickTime = false,
format: customFormatDate,
parse: customParseDate,
}: DateFieldConfig): DateFieldBag => {
const i18n = useContext(DateFieldI18nContext);

const parse = useCallback(
(text: string) => {
text = text.trim();

if (customParseDate) {
return customParseDate(text);
}

if (text.length === 0) {
return null;
}

const date = dayjs(text, [...defaultDateFormats, ...(pickTime ? defaultDateTimeFormats : [])], true);

if (!date.isValid()) {
throw new ConversionError(i18n.invalidInput);
}

return date.toDate();
},
[customParseDate, i18n.invalidInput, pickTime],
);

const format = useCallback(
(value: Date | null | undefined) => {
if (customFormatDate) {
return customFormatDate(value);
}

return formatDate(value, pickTime);
},
[customFormatDate, pickTime],
);

const dateBag = useConverterField({
parse,
format,
name,
validator,
schema,
});

useFieldValidator({
name,
validator: (value) => {
if (required && !(value instanceof Date)) {
return i18n.required;
}

if (!(value instanceof Date)) {
return undefined;
}

if (minDate instanceof Date && dayjs(minDate).diff(dayjs(value)) > 0) {
return i18n.minDate(minDate, pickTime);
}

if (maxDate instanceof Date && dayjs(value).diff(dayjs(maxDate)) > 0) {
return i18n.maxDate(maxDate, pickTime);
}

return undefined;
},
});

return dateBag;
};
11 changes: 11 additions & 0 deletions packages/x/tests/formatDate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { formatDate } from '../src/formatDate';

describe('format date', () => {
it('should format correctly without pickTime option', () => {
expect(formatDate(new Date(2023, 8, 12), false)).toBe('2023.09.12');
});

it('should format correctly with pickTime option', () => {
expect(formatDate(new Date(2023, 8, 12, 17, 27, 42, 42), true)).toBe('2023.09.12 17:27');
});
});
Loading
Loading