Skip to content

Instantly share code, notes, and snippets.

@andymantell
Created February 14, 2020 12:22
Show Gist options
  • Save andymantell/278e67a9b4617e54fe3ddfbf858d0b37 to your computer and use it in GitHub Desktop.
Save andymantell/278e67a9b4617e54fe3ddfbf858d0b37 to your computer and use it in GitHub Desktop.
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 };
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