diff --git a/README.md b/README.md index 658a9445..70ad3eee 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,15 @@ Then, import the components you need to use with Formik in the file you have you - Switch - TextField +Avaliable with [@material-ui/pickers](https://github.com/mui-org/material-ui-pickers) peer dep + +- DatePicker +- KeyboardDatePicker +- TimePicker +- KeyboardTimePicker +- DateTimePicker +- KeyboardDateTimePicker + In this case we will use ``. ```diff diff --git a/package.json b/package.json index e9280fb7..7dbdea32 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "typings": "dist/main.d.ts", "peerDependencies": { "@material-ui/core": ">=4.0.0", + "@material-ui/pickers": ">=3.2.8", "formik": "<2.0.0", "react": "^16.8.0" }, @@ -42,6 +43,9 @@ }, "devDependencies": { "@babel/core": "^7.1.6", + "@date-io/date-fns": "^1.3.11", + "@material-ui/core": "^4.8.3", + "@material-ui/pickers": "^3.2.8", "@material-ui/core": "^4.8.3", "@storybook/addon-actions": "^5.0.11", "@storybook/addon-links": "^5.0.11", @@ -58,6 +62,7 @@ "babel-loader": "^8.0.4", "coveralls": "^3.0.2", "cross-env": "^5.2.0", + "date-fns": "^2.7.0", "eslint": "^5.15.3", "eslint-config-prettier": "^6.0.0", "eslint-plugin-jest": "^22.7.1", diff --git a/src/DatePicker.tsx b/src/DatePicker.tsx new file mode 100644 index 00000000..c589d602 --- /dev/null +++ b/src/DatePicker.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import { + DatePicker as MuiDatePicker, + DatePickerProps as MuiDatePickerProps, +} from '@material-ui/pickers'; + +import { FieldProps, getIn } from 'formik'; + +export type DatePickerProps = FieldProps & + Omit; + +export const fieldToDatePicker = ({ + field, + form, + disabled, + ...props +}: DatePickerProps): MuiDatePickerProps => { + const { name } = field; + const { touched, errors, isSubmitting, setFieldValue, setFieldError } = form; + + const fieldError = getIn(errors, name); + const showError = getIn(touched, name) && !!fieldError; + + return { + ...props, + ...field, + error: showError, + helperText: showError ? fieldError : props.helperText, + disabled: disabled != undefined ? disabled : isSubmitting, + onChange(date) { + setFieldValue(name, date); + }, + onError(error) { + if (error !== fieldError) { + setFieldError(name, String(error)); + } + }, + }; +}; + +export const DatePicker: React.ComponentType = ({ + children, + ...props +}: DatePickerProps) => ( + {children} +); + +DatePicker.displayName = 'FormikMaterialUIDatePicker'; diff --git a/src/DateTimePicker.tsx b/src/DateTimePicker.tsx new file mode 100644 index 00000000..b34329bd --- /dev/null +++ b/src/DateTimePicker.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { + DateTimePicker as MuiDateTimePicker, + DateTimePickerProps as MuiDateTimePickerProps, +} from '@material-ui/pickers'; +import { FieldProps, getIn } from 'formik'; + +export type DateTimePickerProps = FieldProps & + Omit; + +export const fieldToDateTimePicker = ({ + field, + form, + disabled, + ...props +}: DateTimePickerProps): MuiDateTimePickerProps => { + const { name } = field; + const { touched, errors, isSubmitting, setFieldValue, setFieldError } = form; + + const fieldError = getIn(errors, name); + const showError = getIn(touched, name) && !!fieldError; + + return { + ...props, + ...field, + error: showError, + helperText: showError ? fieldError : props.helperText, + disabled: disabled != undefined ? disabled : isSubmitting, + onChange(date) { + setFieldValue(name, date); + }, + onError(error) { + if (error !== fieldError) { + setFieldError(name, String(error)); + } + }, + }; +}; + +export const DateTimePicker: React.ComponentType = ({ + children, + ...props +}: DateTimePickerProps) => ( + + {children} + +); + +DateTimePicker.displayName = 'FormikMaterialUIDateTimePicker'; diff --git a/src/KeyboardDatePicker.tsx b/src/KeyboardDatePicker.tsx new file mode 100644 index 00000000..160c0f14 --- /dev/null +++ b/src/KeyboardDatePicker.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { + KeyboardDatePicker as MuiKeyboardDatePicker, + KeyboardDatePickerProps as MuiKeyboardDatePickerProps, +} from '@material-ui/pickers'; +import { FieldProps, getIn } from 'formik'; + +export type KeyboardDatePickerProps = FieldProps & + Omit; + +export const fieldToKeyboardDatePicker = ({ + field, + form, + disabled, + ...props +}: KeyboardDatePickerProps): MuiKeyboardDatePickerProps => { + const { name } = field; + const { touched, errors, isSubmitting, setFieldValue, setFieldError } = form; + + const fieldError = getIn(errors, name); + const showError = getIn(touched, name) && !!fieldError; + + return { + ...props, + ...field, + error: showError, + helperText: showError ? fieldError : props.helperText, + disabled: disabled != undefined ? disabled : isSubmitting, + onChange(date) { + setFieldValue(name, date); + }, + onError(error) { + if (error !== fieldError) { + setFieldError(name, String(error)); + } + }, + }; +}; + +export const KeyboardDatePicker: React.ComponentType = ({ + children, + ...props +}: KeyboardDatePickerProps) => ( + + {children} + +); + +KeyboardDatePicker.displayName = 'FormikMaterialUIKeyboardDatePicker'; diff --git a/src/KeyboardDateTimePicker.tsx b/src/KeyboardDateTimePicker.tsx new file mode 100644 index 00000000..5827a1c9 --- /dev/null +++ b/src/KeyboardDateTimePicker.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { + KeyboardDateTimePicker as MuiKeyboardDateTimePicker, + KeyboardDateTimePickerProps as MuiKeyboardDateTimePickerProps, +} from '@material-ui/pickers'; +import { FieldProps, getIn } from 'formik'; + +export type KeyboardDateTimePickerProps = FieldProps & + Omit; + +export const fieldToKeyboardDateTimePicker = ({ + field, + form, + disabled, + ...props +}: KeyboardDateTimePickerProps): MuiKeyboardDateTimePickerProps => { + const { name } = field; + const { touched, errors, isSubmitting, setFieldValue, setFieldError } = form; + + const fieldError = getIn(errors, name); + const showError = getIn(touched, name) && !!fieldError; + + return { + ...props, + ...field, + error: showError, + helperText: showError ? fieldError : props.helperText, + disabled: disabled != undefined ? disabled : isSubmitting, + onChange(date) { + setFieldValue(name, date); + }, + onError(error) { + if (error !== fieldError) { + setFieldError(name, String(error)); + } + }, + }; +}; + +export const KeyboardDateTimePicker: React.ComponentType = ({ + children, + ...props +}: KeyboardDateTimePickerProps) => ( + + {children} + +); + +KeyboardDateTimePicker.displayName = 'FormikMaterialUIKeyboardDateTimePicker'; diff --git a/src/KeyboardTimePicker.tsx b/src/KeyboardTimePicker.tsx new file mode 100644 index 00000000..69853ee3 --- /dev/null +++ b/src/KeyboardTimePicker.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { + KeyboardTimePicker as MuiKeyboardTimePicker, + KeyboardTimePickerProps as MuiKeyboardTimePickerProps, +} from '@material-ui/pickers'; +import { FieldProps, getIn } from 'formik'; + +export type KeyboardTimePickerProps = FieldProps & + Omit; + +export const fieldToKeyboardTimePicker = ({ + field, + form, + disabled, + ...props +}: KeyboardTimePickerProps): MuiKeyboardTimePickerProps => { + const { name } = field; + const { touched, errors, isSubmitting, setFieldValue, setFieldError } = form; + + const fieldError = getIn(errors, name); + const showError = getIn(touched, name) && !!fieldError; + + return { + ...props, + ...field, + error: showError, + helperText: showError ? fieldError : props.helperText, + disabled: disabled != undefined ? disabled : isSubmitting, + onChange(date) { + setFieldValue(name, date); + }, + onError(error) { + if (error !== fieldError) { + setFieldError(name, String(error)); + } + }, + }; +}; + +export const KeyboardTimePicker: React.ComponentType = ({ + children, + ...props +}: KeyboardTimePickerProps) => ( + + {children} + +); + +KeyboardTimePicker.displayName = 'FormikMaterialUIKeyboardTimePicker'; diff --git a/src/TimePicker.tsx b/src/TimePicker.tsx new file mode 100644 index 00000000..b048a257 --- /dev/null +++ b/src/TimePicker.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { + TimePicker as MuiTimePicker, + TimePickerProps as MuiTimePickerProps, +} from '@material-ui/pickers'; +import { FieldProps, getIn } from 'formik'; + +export type TimePickerProps = FieldProps & + Omit; + +export const fieldToTimePicker = ({ + field, + form, + disabled, + ...props +}: TimePickerProps): MuiTimePickerProps => { + const { name } = field; + const { touched, errors, isSubmitting, setFieldValue, setFieldError } = form; + + const fieldError = getIn(errors, name); + const showError = getIn(touched, name) && !!fieldError; + + return { + ...props, + ...field, + error: showError, + helperText: showError ? fieldError : props.helperText, + disabled: disabled != undefined ? disabled : isSubmitting, + onChange(date) { + setFieldValue(name, date); + }, + onError(error) { + if (error !== fieldError) { + setFieldError(name, String(error)); + } + }, + }; +}; + +export const TimePicker: React.ComponentType = ({ + children, + ...props +}: TimePickerProps) => ( + {children} +); + +TimePicker.displayName = 'FormikMaterialUITimePicker'; diff --git a/src/__tests__/DatePicker.test.tsx b/src/__tests__/DatePicker.test.tsx new file mode 100644 index 00000000..659e23bc --- /dev/null +++ b/src/__tests__/DatePicker.test.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { Formik, Field, Form } from 'formik'; +import renderer from 'react-test-renderer'; +import { MuiPickersUtilsProvider } from '@material-ui/pickers'; +import DateFnsUtils from '@date-io/date-fns'; + +import { DatePicker } from '../DatePicker'; + +test('DatePicker Renders Correctly', () => { + const component = renderer.create( + + null}> +
+ + +
+
+ ); + + expect(component.toJSON()).toMatchSnapshot(); +}); diff --git a/src/__tests__/DateTimePicker.test.tsx b/src/__tests__/DateTimePicker.test.tsx new file mode 100644 index 00000000..13168bda --- /dev/null +++ b/src/__tests__/DateTimePicker.test.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { Formik, Field, Form } from 'formik'; +import renderer from 'react-test-renderer'; +import { MuiPickersUtilsProvider } from '@material-ui/pickers'; +import DateFnsUtils from '@date-io/date-fns'; + +import { DateTimePicker } from '../DateTimePicker'; + +test('DateTimePicker Renders Correctly', () => { + const component = renderer.create( + + null}> +
+ + +
+
+ ); + + expect(component.toJSON()).toMatchSnapshot(); +}); diff --git a/src/__tests__/KeyboardDatePicker.test.tsx b/src/__tests__/KeyboardDatePicker.test.tsx new file mode 100644 index 00000000..2b1abd43 --- /dev/null +++ b/src/__tests__/KeyboardDatePicker.test.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { Formik, Field, Form } from 'formik'; +import renderer from 'react-test-renderer'; +import { MuiPickersUtilsProvider } from '@material-ui/pickers'; +import DateFnsUtils from '@date-io/date-fns'; + +import { KeyboardDatePicker } from '../KeyboardDatePicker'; + +test('KeyboardDatePicker Renders Correctly', () => { + const component = renderer.create( + + null}> +
+ + +
+
+ ); + + expect(component.toJSON()).toMatchSnapshot(); +}); diff --git a/src/__tests__/KeyboardDateTimePicker.test.tsx b/src/__tests__/KeyboardDateTimePicker.test.tsx new file mode 100644 index 00000000..bfa30df5 --- /dev/null +++ b/src/__tests__/KeyboardDateTimePicker.test.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { Formik, Field, Form } from 'formik'; +import renderer from 'react-test-renderer'; +import { MuiPickersUtilsProvider } from '@material-ui/pickers'; +import DateFnsUtils from '@date-io/date-fns'; + +import { KeyboardDateTimePicker } from '../KeyboardDateTimePicker'; + +test('KeyboardDateTimePicker Renders Correctly', () => { + const component = renderer.create( + + null}> +
+ + +
+
+ ); + + expect(component.toJSON()).toMatchSnapshot(); +}); diff --git a/src/__tests__/KeyboardTimePicker.test.tsx b/src/__tests__/KeyboardTimePicker.test.tsx new file mode 100644 index 00000000..0a438690 --- /dev/null +++ b/src/__tests__/KeyboardTimePicker.test.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { Formik, Field, Form } from 'formik'; +import renderer from 'react-test-renderer'; +import { MuiPickersUtilsProvider } from '@material-ui/pickers'; +import DateFnsUtils from '@date-io/date-fns'; + +import { KeyboardTimePicker } from '../KeyboardTimePicker'; + +test('KeyboardTimePicker Renders Correctly', () => { + const component = renderer.create( + + null}> +
+ + +
+
+ ); + + expect(component.toJSON()).toMatchSnapshot(); +}); diff --git a/src/__tests__/TimePicker.test.tsx b/src/__tests__/TimePicker.test.tsx new file mode 100644 index 00000000..7c6d564e --- /dev/null +++ b/src/__tests__/TimePicker.test.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { Formik, Field, Form } from 'formik'; +import renderer from 'react-test-renderer'; +import { MuiPickersUtilsProvider } from '@material-ui/pickers'; +import DateFnsUtils from '@date-io/date-fns' + +import { TimePicker } from '../TimePicker'; + +test('TimePicker Renders Correctly', () => { + const component = renderer.create( + + null}> +
+ + +
+
+ ); + + expect(component.toJSON()).toMatchSnapshot(); +}); diff --git a/src/main.tsx b/src/main.tsx index efaa4faa..782ef8fd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,3 +6,9 @@ export * from './Select'; export * from './SimpleFileUpload'; export * from './RadioGroup'; export * from './InputBase'; +export * from './DatePicker'; +export * from './KeyboardDatePicker'; +export * from './TimePicker'; +export * from './KeyboardTimePicker'; +export * from './DateTimePicker'; +export * from './KeyboardDateTimePicker'; diff --git a/stories/DatePicker.story.tsx b/stories/DatePicker.story.tsx new file mode 100644 index 00000000..052701bc --- /dev/null +++ b/stories/DatePicker.story.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import Button from '@material-ui/core/Button'; +import { Formik, Field, Form } from 'formik'; +import { LinearProgress } from '@material-ui/core'; +import { action } from '@storybook/addon-actions'; +import * as yup from 'yup'; + +import Wrapper from './Wrapper'; + +import { DatePicker } from '../src/DatePicker'; +import { KeyboardDatePicker } from '../src/KeyboardDatePicker'; +import FormValues from './FormValues'; + +interface Values { + date: Date | null; +} + +const schema = yup.object().shape({ + date: yup.date().required(), +}); + +export default () => ( + + + initialValues={{ + date: new Date(), + }} + validationSchema={schema} + onSubmit={(values, { setSubmitting }) => { + setTimeout(() => { + setSubmitting(false); + action('submit')(values); + }, 2000); + }} + render={({ submitForm, isSubmitting, values }) => ( +
+ +
+ +
+ +
+ {isSubmitting && } +
+ +
+ + + )} + /> +
+); diff --git a/stories/DateTimePicker.story.tsx b/stories/DateTimePicker.story.tsx new file mode 100644 index 00000000..b145ff74 --- /dev/null +++ b/stories/DateTimePicker.story.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import Button from '@material-ui/core/Button'; +import { Formik, Field, Form } from 'formik'; +import { LinearProgress } from '@material-ui/core'; +import { action } from '@storybook/addon-actions'; +import * as yup from 'yup'; + +import Wrapper from './Wrapper'; + +import { DateTimePicker } from '../src/DateTimePicker'; +import { KeyboardDateTimePicker } from '../src/KeyboardDateTimePicker'; +import FormValues from './FormValues'; + +interface Values { + date: Date | null; +} + +const schema = yup.object().shape({ + date: yup.date().required(), +}); + +export default () => ( + + + initialValues={{ + date: new Date(), + }} + validationSchema={schema} + onSubmit={(values, { setSubmitting }) => { + setTimeout(() => { + setSubmitting(false); + action('submit')(values); + }, 2000); + }} + render={({ submitForm, isSubmitting, values }) => ( +
+ +
+ +
+ +
+ {isSubmitting && } +
+ +
+ + + )} + /> +
+); diff --git a/stories/TimePicker.story.tsx b/stories/TimePicker.story.tsx new file mode 100644 index 00000000..e31941b9 --- /dev/null +++ b/stories/TimePicker.story.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import Button from '@material-ui/core/Button'; +import { Formik, Field, Form } from 'formik'; +import { LinearProgress } from '@material-ui/core'; +import { action } from '@storybook/addon-actions'; +import * as yup from 'yup'; + +import Wrapper from './Wrapper'; + +import { TimePicker } from '../src/TimePicker'; +import { KeyboardTimePicker } from '../src/KeyboardTimePicker'; +import FormValues from './FormValues'; + +interface Values { + time: Date | null; +} + +const schema = yup.object().shape({ + time: yup.date().required(), +}); + +export default () => ( + + + initialValues={{ + time: new Date(), + }} + validationSchema={schema} + onSubmit={(values, { setSubmitting }) => { + setTimeout(() => { + setSubmitting(false); + action('submit')(values); + }, 2000); + }} + render={({ submitForm, isSubmitting, values }) => ( +
+ +
+ + {isSubmitting && } +
+ +
+ + + )} + /> +
+); diff --git a/stories/Wrapper.tsx b/stories/Wrapper.tsx index eb409701..b4445825 100644 --- a/stories/Wrapper.tsx +++ b/stories/Wrapper.tsx @@ -7,6 +7,8 @@ import { WithStyles, createStyles, } from '@material-ui/core/styles'; +import { MuiPickersUtilsProvider } from '@material-ui/pickers'; +import DateFnsUtils from '@date-io/date-fns'; const styles = (theme: Theme) => createStyles({ @@ -34,7 +36,9 @@ const Wrapper = ({ title, children }: Props) => ( {title} - {children} + + {children} + ); diff --git a/stories/index.stories.js b/stories/index.stories.js index 7ee5e92d..3c8e2e73 100644 --- a/stories/index.stories.js +++ b/stories/index.stories.js @@ -9,6 +9,9 @@ import SelectorStory from './Selector.story'; import SelectStory from './Select.story'; import SimpleFileUploadStory from './SimpleFileUpload.story'; import RadioGroupStory from './RadioGroup.story.tsx'; +import DatePickerStory from './DatePicker.story.tsx'; +import TimePickerStory from './TimePicker.story.tsx'; +import DateTimePickerStory from './DateTimePicker.story.tsx'; storiesOf('Formik', module) .add('Text Field', () => ) @@ -17,4 +20,7 @@ storiesOf('Formik', module) .add('Selectors', () => ) .add('Select', () => ) .add('Simple File Upload', () => ) - .add('Kitchen Sink', () => ); + .add('Kitchen Sink', () => ) + .add('Date Picker', () => ) + .add('Time Picker', () => ) + .add('Date and Time Picker', () => );