Created
February 14, 2020 12:22
-
-
Save andymantell/278e67a9b4617e54fe3ddfbf858d0b37 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState } from 'react'; | |
import { useFormikContext, useField } from 'formik'; | |
import { DateInput } from 'govuk-react-jsx/govuk'; | |
function DateField(props) { | |
const { values, errors } = useFormikContext(); | |
const { namePrefix, items, ...restProps } = props; | |
const [field, meta, helpers] = useField(namePrefix); // eslint-disable-line no-unused-vars | |
const { setValue, setTouched } = helpers; | |
/** | |
* @param {String} value The value from the formik form state | |
* @returns {Object} The object representation of the date such as {day: '01', month: '02', year: '2013'} | |
*/ | |
const parseValue = value => { | |
const components = {}; | |
if (value) { | |
[components.day, components.month, components.year] = value.split('-'); | |
} | |
return components; | |
}; | |
const [formState, setFormState] = useState(parseValue(values[namePrefix])); | |
/** | |
* @param {Object} dateComponents Object containing day/month/year keys. | |
* Day and month are optional. If day and/or month are missing it will backfill them with 01. No attempt is made to backfill the year - this is the minimum requirement. | |
* @returns {String} String representation of the date in DD-MM-YYYY format | |
*/ | |
const formatValue = dateComponents => { | |
let day; | |
let month; | |
let year; | |
if (items.find(item => item.name === 'year')) { | |
year = dateComponents.year; | |
if (!year) { | |
// If any field is not filled out, return null - the field should be considered "empty" until all are filled | |
return ''; | |
} | |
} | |
if (items.find(item => item.name === 'month')) { | |
month = dateComponents.month; | |
if (month) { | |
month = month.padStart(2, '0'); | |
} else { | |
// If any field is not filled out, return null - the field should be considered "empty" until all are filled | |
return ''; | |
} | |
} else { | |
month = '01'; | |
} | |
if (items.find(item => item.name === 'day')) { | |
day = dateComponents.day; | |
if (day) { | |
day = day.padStart(2, '0'); | |
} else { | |
// If any field is not filled out, return null - the field should be considered "empty" until all are filled | |
return ''; | |
} | |
} else { | |
day = '01'; | |
} | |
return `${day || ''}-${month || ''}-${year}`; | |
}; | |
/** | |
* @param {String} name Name of the date component | |
* @returns {Function} onChange handler for the given date component field | |
*/ | |
const getDateComponentChangeHandler = name => e => { | |
setTouched(true); | |
const newState = { | |
...formState, | |
[name]: e.target.value, | |
}; | |
setFormState(newState); | |
setValue(formatValue(newState)); | |
}; | |
// Bind change and blur handlers to each of the date components | |
// As well as initialising their value from the state | |
items.forEach(item => { | |
item.onChange = getDateComponentChangeHandler(item.name); | |
item.onBlur = getDateComponentChangeHandler(item.name); | |
item.value = formState[item.name] || ''; // TODO: Can we avoid this little trick? Currently need to short circuit because this starts out as undefined, which causes React to render an uncontrolled component. When a value then appears, React switches it to controlled and throws a warning | |
}); | |
return ( | |
<DateInput | |
{...restProps} | |
items={items} | |
tabIndex={-1} | |
style={{ | |
outline: 'none', | |
}} | |
onFocus={e => { | |
// Formik raises form errors against the id of the field collection, but no such input exists with that id | |
// Therefore if this element receives focus from an error summary link, we move the focus onto the first input element | |
if ( | |
e.relatedTarget && | |
e.relatedTarget.nodeName === 'A' && | |
e.target.id === restProps.id | |
) { | |
e.target.querySelector('input').focus(); | |
} | |
}} | |
{...(errors[namePrefix] && { | |
errorMessage: { | |
children: errors[namePrefix], | |
}, | |
})} | |
/> | |
); | |
} | |
export { DateField }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import { render, fireEvent, wait } from '@testing-library/react'; | |
import { Formik } from 'formik'; | |
import { DateField } from './DateField'; | |
const renderForm = (props, initialValues) => | |
render( | |
<Formik initialValues={initialValues}> | |
{() => <DateField {...props} />} | |
</Formik> | |
); | |
describe('DateField', () => { | |
describe('with a year', () => { | |
it('correctly renders an empty field', () => { | |
// ! arrange | |
const props = { | |
id: 'datefrom', | |
namePrefix: 'datefrom', | |
items: [ | |
{ | |
name: 'year', | |
}, | |
], | |
}; | |
// ! act | |
const { container, getByText, queryByText } = renderForm(props, {}); | |
// ! assert | |
expect(queryByText('Day')).toBeNull(); | |
expect(queryByText('Month')).toBeNull(); | |
expect(getByText('Year')).toBeInTheDocument(); | |
expect(container.querySelector('#datefrom-year').value).toEqual(''); | |
}); | |
it('correctly renders a populated field', () => { | |
// ! arrange | |
const props = { | |
id: 'datefrom', | |
namePrefix: 'datefrom', | |
items: [ | |
{ | |
name: 'year', | |
}, | |
], | |
}; | |
// ! act | |
const { container, getByText, queryByText } = renderForm(props, { | |
datefrom: '01-01-2020', | |
}); | |
// ! assert | |
expect(queryByText('Day')).toBeNull(); | |
expect(queryByText('Month')).toBeNull(); | |
expect(getByText('Year')).toBeInTheDocument(); | |
expect(container.querySelector('#datefrom-year').value).toEqual('2020'); | |
}); | |
it('correctly submits a full date value', async () => { | |
// ! arrange | |
const props = { | |
id: 'datefrom', | |
namePrefix: 'datefrom', | |
items: [ | |
{ | |
name: 'year', | |
}, | |
], | |
}; | |
const submitHandler = jest.fn().mockResolvedValueOnce({}); | |
// ! act | |
const { container, getByLabelText } = render( | |
<Formik initialValues={{}} onSubmit={submitHandler}> | |
{({ handleSubmit }) => ( | |
<form onSubmit={handleSubmit}> | |
<DateField {...props} /> | |
</form> | |
)} | |
</Formik> | |
); | |
await wait(() => { | |
fireEvent.change(getByLabelText('Year'), { target: { value: '2019' } }); | |
fireEvent.submit(container.querySelector('form')); | |
}); | |
// ! assert | |
expect(submitHandler).toHaveBeenCalledWith( | |
{ datefrom: '01-01-2019' }, | |
expect.anything() | |
); | |
}); | |
it('allows you to clear the field', async () => { | |
// ! arrange | |
const props = { | |
id: 'datefrom', | |
namePrefix: 'datefrom', | |
items: [ | |
{ | |
name: 'year', | |
}, | |
], | |
}; | |
const submitHandler = jest.fn().mockResolvedValueOnce({}); | |
// ! act | |
const { container, getByLabelText } = render( | |
<Formik | |
initialValues={{ datefrom: '20-12-2019' }} | |
onSubmit={submitHandler} | |
> | |
{({ handleSubmit }) => ( | |
<form onSubmit={handleSubmit}> | |
<DateField {...props} /> | |
</form> | |
)} | |
</Formik> | |
); | |
await wait(() => { | |
fireEvent.change(getByLabelText('Year'), { target: { value: '' } }); | |
fireEvent.submit(container.querySelector('form')); | |
}); | |
// ! assert | |
expect(submitHandler).toHaveBeenCalledWith( | |
{ datefrom: '' }, | |
expect.anything() | |
); | |
}); | |
}); | |
describe('with month and year', () => { | |
it('correctly renders an empty field', () => { | |
// ! arrange | |
const props = { | |
id: 'datefrom', | |
namePrefix: 'datefrom', | |
items: [ | |
{ | |
name: 'month', | |
}, | |
{ | |
name: 'year', | |
}, | |
], | |
}; | |
// ! act | |
const { container, getByText, queryByText } = renderForm(props, {}); | |
// ! assert | |
expect(queryByText('Day')).toBeNull(); | |
expect(getByText('Month')).toBeInTheDocument(); | |
expect(getByText('Year')).toBeInTheDocument(); | |
expect(container.querySelector('#datefrom-month').value).toEqual(''); | |
expect(container.querySelector('#datefrom-year').value).toEqual(''); | |
}); | |
it('correctly renders a populated field', () => { | |
// ! arrange | |
const props = { | |
id: 'datefrom', | |
namePrefix: 'datefrom', | |
items: [ | |
{ | |
name: 'month', | |
}, | |
{ | |
name: 'year', | |
}, | |
], | |
}; | |
// ! act | |
const { container, getByText, queryByText } = renderForm(props, { | |
datefrom: '01-01-2020', | |
}); | |
// ! assert | |
expect(queryByText('Day')).toBeNull(); | |
expect(getByText('Month')).toBeInTheDocument(); | |
expect(getByText('Year')).toBeInTheDocument(); | |
expect(container.querySelector('#datefrom-month').value).toEqual('01'); | |
expect(container.querySelector('#datefrom-year').value).toEqual('2020'); | |
}); | |
it('correctly submits a full date value', async () => { | |
// ! arrange | |
const props = { | |
id: 'datefrom', | |
namePrefix: 'datefrom', | |
items: [ | |
{ | |
name: 'month', | |
}, | |
{ | |
name: 'year', | |
}, | |
], | |
}; | |
const submitHandler = jest.fn().mockResolvedValueOnce({}); | |
// ! act | |
const { container, getByLabelText } = render( | |
<Formik initialValues={{}} onSubmit={submitHandler}> | |
{({ handleSubmit }) => ( | |
<form onSubmit={handleSubmit}> | |
<DateField {...props} /> | |
</form> | |
)} | |
</Formik> | |
); | |
await wait(() => { | |
fireEvent.change(getByLabelText('Year'), { target: { value: '2019' } }); | |
fireEvent.change(getByLabelText('Month'), { target: { value: '12' } }); | |
fireEvent.submit(container.querySelector('form')); | |
}); | |
// ! assert | |
expect(submitHandler).toHaveBeenCalledWith( | |
{ datefrom: '01-12-2019' }, | |
expect.anything() | |
); | |
}); | |
it('allows you to clear the field', async () => { | |
// ! arrange | |
const props = { | |
id: 'datefrom', | |
namePrefix: 'datefrom', | |
items: [ | |
{ | |
name: 'month', | |
}, | |
{ | |
name: 'year', | |
}, | |
], | |
}; | |
const submitHandler = jest.fn().mockResolvedValueOnce({}); | |
// ! act | |
const { container, getByLabelText } = render( | |
<Formik | |
initialValues={{ datefrom: '20-12-2019' }} | |
onSubmit={submitHandler} | |
> | |
{({ handleSubmit }) => ( | |
<form onSubmit={handleSubmit}> | |
<DateField {...props} /> | |
</form> | |
)} | |
</Formik> | |
); | |
await wait(() => { | |
fireEvent.change(getByLabelText('Year'), { target: { value: '' } }); | |
fireEvent.change(getByLabelText('Month'), { target: { value: '' } }); | |
fireEvent.submit(container.querySelector('form')); | |
}); | |
// ! assert | |
expect(submitHandler).toHaveBeenCalledWith( | |
{ datefrom: '' }, | |
expect.anything() | |
); | |
}); | |
}); | |
describe('with day, month and year', () => { | |
it('correctly renders an empty field', () => { | |
// ! arrange | |
const props = { | |
id: 'datefrom', | |
namePrefix: 'datefrom', | |
items: [ | |
{ | |
name: 'day', | |
}, | |
{ | |
name: 'month', | |
}, | |
{ | |
name: 'year', | |
}, | |
], | |
}; | |
// ! act | |
const { container, getByText } = renderForm(props, {}); | |
// ! assert | |
expect(getByText('Day')).toBeInTheDocument(); | |
expect(getByText('Month')).toBeInTheDocument(); | |
expect(getByText('Year')).toBeInTheDocument(); | |
expect(container.querySelector('#datefrom-day').value).toEqual(''); | |
expect(container.querySelector('#datefrom-month').value).toEqual(''); | |
expect(container.querySelector('#datefrom-year').value).toEqual(''); | |
}); | |
it('correctly renders a populated field', () => { | |
// ! arrange | |
const props = { | |
id: 'datefrom', | |
namePrefix: 'datefrom', | |
items: [ | |
{ | |
name: 'day', | |
}, | |
{ | |
name: 'month', | |
}, | |
{ | |
name: 'year', | |
}, | |
], | |
}; | |
// ! act | |
const { container, getByText } = renderForm(props, { | |
datefrom: '09-01-2020', | |
}); | |
// ! assert | |
expect(getByText('Day')).toBeInTheDocument(); | |
expect(getByText('Month')).toBeInTheDocument(); | |
expect(getByText('Year')).toBeInTheDocument(); | |
expect(container.querySelector('#datefrom-day').value).toEqual('09'); | |
expect(container.querySelector('#datefrom-month').value).toEqual('01'); | |
expect(container.querySelector('#datefrom-year').value).toEqual('2020'); | |
}); | |
it('correctly submits a full date value', async () => { | |
// ! arrange | |
const props = { | |
id: 'datefrom', | |
namePrefix: 'datefrom', | |
items: [ | |
{ | |
name: 'day', | |
}, | |
{ | |
name: 'month', | |
}, | |
{ | |
name: 'year', | |
}, | |
], | |
}; | |
const submitHandler = jest.fn().mockResolvedValueOnce({}); | |
// ! act | |
const { container, getByLabelText } = render( | |
<Formik initialValues={{}} onSubmit={submitHandler}> | |
{({ handleSubmit }) => ( | |
<form onSubmit={handleSubmit}> | |
<DateField {...props} /> | |
</form> | |
)} | |
</Formik> | |
); | |
await wait(() => { | |
fireEvent.change(getByLabelText('Year'), { target: { value: '2019' } }); | |
fireEvent.change(getByLabelText('Month'), { target: { value: '12' } }); | |
fireEvent.change(getByLabelText('Day'), { target: { value: '20' } }); | |
fireEvent.submit(container.querySelector('form')); | |
}); | |
// ! assert | |
expect(submitHandler).toHaveBeenCalledWith( | |
{ datefrom: '20-12-2019' }, | |
expect.anything() | |
); | |
}); | |
it('allows you to clear the field', async () => { | |
// ! arrange | |
const props = { | |
id: 'datefrom', | |
namePrefix: 'datefrom', | |
items: [ | |
{ | |
name: 'day', | |
}, | |
{ | |
name: 'month', | |
}, | |
{ | |
name: 'year', | |
}, | |
], | |
}; | |
const submitHandler = jest.fn().mockResolvedValueOnce({}); | |
// ! act | |
const { container, getByLabelText } = render( | |
<Formik | |
initialValues={{ datefrom: '20-12-2019' }} | |
onSubmit={submitHandler} | |
> | |
{({ handleSubmit }) => ( | |
<form onSubmit={handleSubmit}> | |
<DateField {...props} /> | |
</form> | |
)} | |
</Formik> | |
); | |
await wait(() => { | |
fireEvent.change(getByLabelText('Year'), { target: { value: '' } }); | |
fireEvent.change(getByLabelText('Month'), { target: { value: '' } }); | |
fireEvent.change(getByLabelText('Day'), { target: { value: '' } }); | |
fireEvent.submit(container.querySelector('form')); | |
}); | |
// ! assert | |
expect(submitHandler).toHaveBeenCalledWith( | |
{ datefrom: '' }, | |
expect.anything() | |
); | |
}); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment