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

[RFR] Migrate DateInput to use useInput hook #3511

Merged
merged 6 commits into from
Aug 13, 2019
Merged
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
2 changes: 1 addition & 1 deletion examples/simple/src/posts/PostCreate.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const PostCreateToolbar = props => (

const backlinksDefaultValue = [
{
date: new Date().toISOString(),
date: new Date(),
url: 'http://google.com',
},
];
Expand Down
1 change: 1 addition & 0 deletions packages/ra-ui-materialui/src/input/BooleanInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const BooleanInput: FunctionComponent<
source,
type: 'checkbox',
validate,
...rest,
});

const handleChange = useCallback(
Expand Down
60 changes: 27 additions & 33 deletions packages/ra-ui-materialui/src/input/DateInput.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useCallback } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import TextField from '@material-ui/core/TextField';
import { addField, FieldTitle } from 'ra-core';
import { useInput, FieldTitle } from 'ra-core';

import sanitizeRestProps from './sanitizeRestProps';
import InputHelperText from './InputHelperText';
Expand All @@ -12,7 +12,7 @@ import InputHelperText from './InputHelperText';
* @param {Date} v value to convert
* @returns {String} A standardized date (yyyy-MM-dd), to be passed to an <input type="date" />
*/
const dateFormatter = v => {
const convertDateToString = v => {
if (!(v instanceof Date) || isNaN(v.getDate())) return;
const pad = '00';
const yyyy = v.getFullYear().toString();
Expand All @@ -23,59 +23,59 @@ const dateFormatter = v => {

const dateRegex = /^\d{4}-\d{2}-\d{2}$/;

const sanitizeValue = value => {
const format = value => {
// null, undefined and empty string values should not go through dateFormatter
// otherwise, it returns undefined and will make the input an uncontrolled one.
if (value == null || value === '') {
return '';
}

if (value instanceof Date) {
return dateFormatter(value);
return convertDateToString(value);
}

// valid dates should not be converted
if (dateRegex.test(value)) {
return value;
}

return dateFormatter(new Date(value));
return convertDateToString(new Date(value));
};

export const DateInput = ({
className,
meta,
input,
isRequired,
label,
options,
source,
resource,
helperText,
onBlur,
onChange,
onFocus,
validate,
...rest
}) => {
const handleChange = useCallback(
event => {
input.onChange(event.target.value);
},
[input]
);

if (typeof meta === 'undefined') {
throw new Error(
"The DateInput component wasn't called within a react-final-form <Field>. Did you decorate it and forget to add the addField prop to your component? See https://marmelab.com/react-admin/Inputs.html#writing-your-own-input-component for details."
);
}
const { touched, error } = meta;
const value = sanitizeValue(input.value);
const {
id,
input,
isRequired,
meta: { error, touched },
} = useInput({
format,
onBlur,
onChange,
onFocus,
resource,
source,
validate,
...rest,
});

return (
<TextField
id={id}
{...input}
className={className}
type="date"
margin="normal"
id={`${resource}_${source}_date_input`}
error={!!(touched && error)}
helperText={
<InputHelperText
Expand All @@ -97,19 +97,13 @@ export const DateInput = ({
}}
{...options}
{...sanitizeRestProps(rest)}
value={value}
onChange={handleChange}
/>
);
};

DateInput.propTypes = {
classes: PropTypes.object,
className: PropTypes.string,
input: PropTypes.object,
isRequired: PropTypes.bool,
label: PropTypes.string,
meta: PropTypes.object,
options: PropTypes.object,
resource: PropTypes.string,
source: PropTypes.string,
Expand All @@ -119,4 +113,4 @@ DateInput.defaultProps = {
options: {},
};

export default addField(DateInput);
export default DateInput;
67 changes: 45 additions & 22 deletions packages/ra-ui-materialui/src/input/DateInput.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,76 @@ import React from 'react';
import expect from 'expect';
import { render, fireEvent, cleanup } from '@testing-library/react';

import { DateInput } from './DateInput';
import DateInput from './DateInput';
import { Form } from 'react-final-form';
import { required } from 'ra-core';

describe('<DateInput />', () => {
const defaultProps = {
resource: 'bar',
source: 'foo',
meta: {},
input: {},
translate: x => x,
resource: 'posts',
source: 'publishedAt',
};

afterEach(cleanup);

it('should render a date input', () => {
const { getByLabelText } = render(<DateInput {...defaultProps} />);
expect(getByLabelText('resources.bar.fields.foo').type).toBe('date');
const { getByLabelText } = render(
<Form
onSubmit={jest.fn}
render={() => <DateInput {...defaultProps} />}
/>
);
expect(getByLabelText('resources.posts.fields.publishedAt').type).toBe(
'date'
);
});

it('should call `input.onChange` method when changed', () => {
const onChange = jest.fn();
let formApi;
const { getByLabelText } = render(
<DateInput {...defaultProps} input={{ onChange }} />
<Form
onSubmit={jest.fn()}
render={({ form }) => {
formApi = form;
return <DateInput {...defaultProps} />;
}}
/>
);
const input = getByLabelText('resources.bar.fields.foo');
fireEvent.change(input, { target: { value: '2010-01-04' } });
expect(onChange.mock.calls[0][0]).toBe('2010-01-04');
const input = getByLabelText('resources.posts.fields.publishedAt');
fireEvent.change(input, {
target: { value: '2010-01-04' },
});
expect(formApi.getState().values.publishedAt).toEqual('2010-01-04');
});

describe('error message', () => {
it('should not be displayed if field is pristine', () => {
const { queryByText } = render(
<DateInput
{...defaultProps}
meta={{ touched: false, error: 'Required field.' }}
<Form
onSubmit={jest.fn}
render={() => (
<DateInput {...defaultProps} validate={required()} />
)}
/>
);
expect(queryByText('Required field.')).toBeNull();
expect(queryByText('ra.validation.required')).toBeNull();
});

it('should be displayed if field has been touched and is invalid', () => {
const { queryByText } = render(
<DateInput
{...defaultProps}
meta={{ touched: true, error: 'Required field.' }}
const { getByLabelText, queryByText } = render(
<Form
onSubmit={jest.fn}
validateOnBlur
render={() => (
<DateInput {...defaultProps} validate={required()} />
)}
/>
);
expect(queryByText('Required field.')).toBeDefined();
const input = getByLabelText(
'resources.posts.fields.publishedAt *'
);
fireEvent.blur(input);
expect(queryByText('ra.validation.required')).not.toBeNull();
});
});
});