Skip to content

Commit 414c4ad

Browse files
author
React-Admin CI
committed
Fix <DateInput> and <DateTimeInput> do not react to form changes
1 parent 60d2de9 commit 414c4ad

File tree

6 files changed

+173
-49
lines changed

6 files changed

+173
-49
lines changed

packages/ra-ui-materialui/src/input/DateInput.spec.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useFormState } from 'react-hook-form';
88
import { AdminContext } from '../AdminContext';
99
import { SimpleForm } from '../form';
1010
import { DateInput } from './DateInput';
11-
import { Basic, Parse } from './DateInput.stories';
11+
import { Basic, ExternalChanges, Parse } from './DateInput.stories';
1212

1313
describe('<DateInput />', () => {
1414
const defaultProps = {
@@ -246,6 +246,25 @@ describe('<DateInput />', () => {
246246
});
247247
});
248248

249+
it('should change its value when the form value has changed', async () => {
250+
render(
251+
<ExternalChanges
252+
simpleFormProps={{
253+
defaultValues: { publishedAt: '2021-09-11' },
254+
}}
255+
/>
256+
);
257+
await screen.findByText('"2021-09-11" (string)');
258+
const input = screen.getByLabelText('Published at') as HTMLInputElement;
259+
fireEvent.change(input, {
260+
target: { value: '2021-10-30' },
261+
});
262+
fireEvent.blur(input);
263+
await screen.findByText('"2021-10-30" (string)');
264+
fireEvent.click(screen.getByText('Change value'));
265+
await screen.findByText('"2021-10-20" (string)');
266+
});
267+
249268
describe('error message', () => {
250269
it('should not be displayed if field is pristine', () => {
251270
render(<Basic dateInputProps={{ validate: required() }} />);

packages/ra-ui-materialui/src/input/DateInput.stories.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import * as React from 'react';
22
import polyglotI18nProvider from 'ra-i18n-polyglot';
33
import englishMessages from 'ra-language-english';
4-
import { minValue } from 'ra-core';
4+
import { minValue, useRecordContext } from 'ra-core';
5+
import { useFormContext, useWatch } from 'react-hook-form';
6+
import { Box, Button, Typography } from '@mui/material';
7+
import get from 'lodash/get';
58

69
import { AdminContext } from '../AdminContext';
710
import { Create } from '../detail';
@@ -100,6 +103,19 @@ export const Parse = ({ simpleFormProps }) => (
100103
</Wrapper>
101104
);
102105

106+
export const ExternalChanges = ({
107+
simpleFormProps = {
108+
defaultValues: { publishedAt: '2021-09-11' },
109+
},
110+
}: {
111+
simpleFormProps?: Omit<SimpleFormProps, 'children'>;
112+
}) => (
113+
<Wrapper simpleFormProps={simpleFormProps}>
114+
<DateInput source="publishedAt" />
115+
<DateHelper source="publishedAt" value="2021-10-20" />
116+
</Wrapper>
117+
);
118+
103119
const i18nProvider = polyglotI18nProvider(() => englishMessages);
104120

105121
const Wrapper = ({
@@ -118,3 +134,37 @@ const Wrapper = ({
118134
</Create>
119135
</AdminContext>
120136
);
137+
138+
const DateHelper = ({ source, value }: { source: string; value: string }) => {
139+
const record = useRecordContext();
140+
const { resetField, setValue } = useFormContext();
141+
const currentValue = useWatch({ name: source });
142+
143+
return (
144+
<Box>
145+
<Typography>
146+
Record value: {get(record, source)?.toString() ?? '-'}
147+
</Typography>
148+
<Typography>
149+
Current value: {currentValue?.toString() ?? '-'}
150+
</Typography>
151+
<Button
152+
onClick={() => {
153+
setValue(source, value, { shouldDirty: true });
154+
}}
155+
type="button"
156+
>
157+
Change value
158+
</Button>
159+
<Button
160+
color="error"
161+
onClick={() => {
162+
resetField(source);
163+
}}
164+
type="button"
165+
>
166+
Reset
167+
</Button>
168+
</Box>
169+
);
170+
};

packages/ra-ui-materialui/src/input/DateInput.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,25 +71,20 @@ export const DateInput = ({
7171
const valueChangedFromInput = React.useRef(false);
7272
const localInputRef = React.useRef<HTMLInputElement>();
7373
const initialDefaultValueRef = React.useRef(field.value);
74+
const currentValueRef = React.useRef(field.value);
7475

7576
// update the react-hook-form value if the field value changes
7677
React.useEffect(() => {
77-
const initialDateValue =
78-
new Date(initialDefaultValueRef.current).getTime() || null;
79-
80-
const fieldDateValue = new Date(field.value).getTime() || null;
81-
8278
if (
83-
initialDateValue !== fieldDateValue &&
79+
currentValueRef.current !== field.value &&
8480
!valueChangedFromInput.current
8581
) {
8682
setRenderCount(r => r + 1);
87-
field.onChange(field.value);
8883
initialDefaultValueRef.current = field.value;
89-
valueChangedFromInput.current = false;
84+
currentValueRef.current = field.value;
9085
}
86+
valueChangedFromInput.current = false;
9187
}, [setRenderCount, field]);
92-
9388
const { onBlur: onBlurFromField } = field;
9489
const hasFocus = React.useRef(false);
9590

@@ -119,6 +114,7 @@ export const DateInput = ({
119114
if (newValue !== '' && newValue != null && isNewValueValid) {
120115
field.onChange(newValue);
121116
valueChangedFromInput.current = true;
117+
currentValueRef.current = newValue;
122118
}
123119
}
124120
);

packages/ra-ui-materialui/src/input/DateTimeInput.spec.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { SimpleForm, Toolbar } from '../form';
1010
import { DateTimeInput } from './DateTimeInput';
1111
import { ArrayInput, SimpleFormIterator } from './ArrayInput';
1212
import { SaveButton } from '../button';
13+
import { ExternalChanges } from './DateTimeInput.stories';
1314

1415
describe('<DateTimeInput />', () => {
1516
const defaultProps = {
@@ -197,6 +198,25 @@ describe('<DateTimeInput />', () => {
197198
});
198199
});
199200

201+
it('should change its value when the form value has changed', async () => {
202+
render(
203+
<ExternalChanges
204+
simpleFormProps={{
205+
defaultValues: { published: '2021-09-11 20:00:00' },
206+
}}
207+
/>
208+
);
209+
await screen.findByText('"2021-09-11 20:00:00" (string)');
210+
const input = screen.getByLabelText('Published') as HTMLInputElement;
211+
fireEvent.change(input, {
212+
target: { value: '2021-10-30 09:00:00' },
213+
});
214+
fireEvent.blur(input);
215+
await screen.findByText('"2021-10-30T09:00" (string)');
216+
fireEvent.click(screen.getByText('Change value'));
217+
await screen.findByText('"2021-10-20 10:00:00" (string)');
218+
});
219+
200220
describe('error message', () => {
201221
it('should not be displayed if field is pristine', () => {
202222
render(

packages/ra-ui-materialui/src/input/DateTimeInput.stories.tsx

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import * as React from 'react';
22
import polyglotI18nProvider from 'ra-i18n-polyglot';
33
import englishMessages from 'ra-language-english';
4+
import { useRecordContext } from 'ra-core';
5+
import { useFormContext, useWatch } from 'react-hook-form';
6+
import { Box, Button, Typography } from '@mui/material';
7+
import get from 'lodash/get';
48

59
import { AdminContext } from '../AdminContext';
610
import { Create } from '../detail';
7-
import { SimpleForm } from '../form';
11+
import { SimpleForm, SimpleFormProps } from '../form';
812
import { DateTimeInput } from './DateTimeInput';
913
import { FormInspector } from './common';
1014

@@ -44,15 +48,68 @@ export const ReadOnly = () => (
4448
</Wrapper>
4549
);
4650

51+
export const ExternalChanges = ({
52+
simpleFormProps = {
53+
defaultValues: { published: '2021-09-11 20:00:00' },
54+
},
55+
}: {
56+
simpleFormProps?: Omit<SimpleFormProps, 'children'>;
57+
}) => (
58+
<Wrapper simpleFormProps={simpleFormProps}>
59+
<DateTimeInput source="published" />
60+
<DateHelper source="published" value="2021-10-20 10:00:00" />
61+
</Wrapper>
62+
);
63+
4764
const i18nProvider = polyglotI18nProvider(() => englishMessages);
4865

49-
const Wrapper = ({ children }) => (
66+
const Wrapper = ({
67+
children,
68+
simpleFormProps,
69+
}: {
70+
children: React.ReactNode;
71+
simpleFormProps?: Omit<SimpleFormProps, 'children'>;
72+
}) => (
5073
<AdminContext i18nProvider={i18nProvider} defaultTheme="light">
5174
<Create resource="posts">
52-
<SimpleForm>
75+
<SimpleForm {...simpleFormProps}>
5376
{children}
5477
<FormInspector name="published" />
5578
</SimpleForm>
5679
</Create>
5780
</AdminContext>
5881
);
82+
83+
const DateHelper = ({ source, value }: { source: string; value: string }) => {
84+
const record = useRecordContext();
85+
const { resetField, setValue } = useFormContext();
86+
const currentValue = useWatch({ name: source });
87+
88+
return (
89+
<Box>
90+
<Typography>
91+
Record value: {get(record, source)?.toString() ?? '-'}
92+
</Typography>
93+
<Typography>
94+
Current value: {currentValue?.toString() ?? '-'}
95+
</Typography>
96+
<Button
97+
onClick={() => {
98+
setValue(source, value, { shouldDirty: true });
99+
}}
100+
type="button"
101+
>
102+
Change value
103+
</Button>
104+
<Button
105+
color="error"
106+
onClick={() => {
107+
resetField(source);
108+
}}
109+
type="button"
110+
>
111+
Reset
112+
</Button>
113+
</Box>
114+
);
115+
};

packages/ra-ui-materialui/src/input/DateTimeInput.tsx

Lines changed: 17 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -53,24 +53,18 @@ export const DateTimeInput = ({
5353
const valueChangedFromInput = React.useRef(false);
5454
const localInputRef = React.useRef<HTMLInputElement>();
5555
const initialDefaultValueRef = React.useRef(field.value);
56+
const currentValueRef = React.useRef(field.value);
5657

5758
React.useEffect(() => {
58-
const initialDateValue =
59-
new Date(initialDefaultValueRef.current).getTime() || null;
60-
61-
const fieldDateValue = new Date(field.value).getTime() || null;
62-
6359
if (
64-
initialDateValue !== fieldDateValue &&
60+
currentValueRef.current !== field.value &&
6561
!valueChangedFromInput.current
6662
) {
6763
setRenderCount(r => r + 1);
68-
parse
69-
? field.onChange(parse(field.value))
70-
: field.onChange(field.value);
7164
initialDefaultValueRef.current = field.value;
72-
valueChangedFromInput.current = false;
65+
currentValueRef.current = field.value;
7366
}
67+
valueChangedFromInput.current = false;
7468
}, [setRenderCount, parse, field]);
7569

7670
const { onBlur: onBlurFromField } = field;
@@ -88,23 +82,17 @@ export const DateTimeInput = ({
8882
return;
8983
}
9084
const target = event.target;
85+
const newValue = target.value;
86+
const isNewValueValid =
87+
newValue === '' || !isNaN(new Date(target.value).getTime());
9188

92-
const newValue =
93-
target.valueAsDate !== undefined &&
94-
target.valueAsDate !== null &&
95-
!isNaN(new Date(target.valueAsDate).getTime())
96-
? parse
97-
? parse(target.valueAsDate)
98-
: target.valueAsDate
99-
: parse
100-
? parse(target.value)
101-
: formatDateTime(target.value);
102-
103-
// Some browsers will return null for an invalid date so we only change react-hook-form value if it's not null
89+
// Some browsers will return null for an invalid date
90+
// so we only change react-hook-form value if it's not null.
10491
// The input reset is handled in the onBlur event handler
105-
if (newValue !== '' && newValue != null) {
92+
if (newValue !== '' && newValue != null && isNewValueValid) {
10693
field.onChange(newValue);
10794
valueChangedFromInput.current = true;
95+
currentValueRef.current = newValue;
10896
}
10997
};
11098

@@ -122,20 +110,14 @@ export const DateTimeInput = ({
122110
return;
123111
}
124112

113+
const newValue = localInputRef.current.value;
125114
// To ensure users can clear the input, we check its value on blur
126115
// and submit it to react-hook-form
127-
const newValue =
128-
localInputRef.current.valueAsDate !== undefined &&
129-
localInputRef.current.valueAsDate !== null &&
130-
!isNaN(new Date(localInputRef.current.valueAsDate).getTime())
131-
? parse
132-
? parse(localInputRef.current.valueAsDate)
133-
: formatDateTime(localInputRef.current.valueAsDate)
134-
: parse
135-
? parse(localInputRef.current.value)
136-
: formatDateTime(localInputRef.current.value);
137-
138-
if (newValue !== field.value) {
116+
const isNewValueValid =
117+
newValue === '' ||
118+
!isNaN(new Date(localInputRef.current.value).getTime());
119+
120+
if (isNewValueValid && field.value !== newValue) {
139121
field.onChange(newValue ?? '');
140122
}
141123

0 commit comments

Comments
 (0)