Skip to content

Commit 8d9b5c4

Browse files
committed
Fix ArrayInput root error display on conditional mount
1 parent 8e725c8 commit 8d9b5c4

3 files changed

Lines changed: 121 additions & 11 deletions

File tree

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event';
44
import {
55
RecordContextProvider,
66
ResourceContextProvider,
7+
required,
78
testDataProvider,
89
useArrayInput,
910
} from 'ra-core';
@@ -283,6 +284,56 @@ describe('<ArrayInput />', () => {
283284
expect(screen.queryByText('test helper text')).not.toBeNull();
284285
});
285286

287+
it('should not display a root-level array error immediately when mounted in onChange mode', async () => {
288+
const submit = jest.fn();
289+
290+
const FormWithConditionalArrayInput = () => {
291+
const [showArrayInput, setShowArrayInput] = React.useState(false);
292+
293+
return (
294+
<>
295+
<button
296+
onClick={() => setShowArrayInput(true)}
297+
type="button"
298+
>
299+
Show array input
300+
</button>
301+
{showArrayInput ? (
302+
<ArrayInput source="authors" validate={required()}>
303+
<SimpleFormIterator>
304+
<TextInput source="name" />
305+
</SimpleFormIterator>
306+
</ArrayInput>
307+
) : null}
308+
</>
309+
);
310+
};
311+
312+
render(
313+
<AdminContext dataProvider={testDataProvider()}>
314+
<ResourceContextProvider value="books">
315+
<SimpleForm mode="onChange" onSubmit={submit}>
316+
<FormWithConditionalArrayInput />
317+
</SimpleForm>
318+
</ResourceContextProvider>
319+
</AdminContext>
320+
);
321+
322+
fireEvent.click(screen.getByText('Show array input'));
323+
324+
await screen.findByText('resources.books.fields.authors');
325+
expect(screen.queryByText('ra.validation.required')).toBeNull();
326+
327+
fireEvent.click(await screen.findByLabelText('ra.action.add'));
328+
fireEvent.click(await screen.findByLabelText('ra.action.remove'));
329+
330+
await screen.findByText('ra.validation.required');
331+
332+
fireEvent.click(screen.getByText('ra.action.save'));
333+
334+
await screen.findByText('ra.validation.required');
335+
});
336+
286337
it('should update the form state to dirty, and allow submit, on updating an array input with default value', async () => {
287338
render(
288339
<AdminContext dataProvider={testDataProvider()}>

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
TestMemoryRouter,
1515
useSourceContext,
1616
} from 'ra-core';
17-
import { Button, InputAdornment, Stack } from '@mui/material';
17+
import { Alert, Button, InputAdornment, Stack } from '@mui/material';
1818

1919
import { Edit, Create } from '../../detail';
2020
import { SimpleForm, TabbedForm } from '../../form';
@@ -825,6 +825,47 @@ export const GlobalValidation = () => (
825825
</TestMemoryRouter>
826826
);
827827

828+
const BookCreateConditionalValidation = () => {
829+
const [showArrayInput, setShowArrayInput] = React.useState(false);
830+
831+
return (
832+
<Create>
833+
<SimpleForm mode="onChange">
834+
<Alert severity="info" sx={{ mb: 2 }}>
835+
Reproduccion del bug 2: mostrar el ArrayInput no debe
836+
pintarlo invalido automaticamente. El error debe aparecer
837+
recien despues de interaccion real o submit invalido.
838+
</Alert>
839+
<Button
840+
onClick={() => setShowArrayInput(true)}
841+
variant="outlined"
842+
>
843+
Show array input
844+
</Button>
845+
{showArrayInput ? (
846+
<ArrayInput
847+
source="authors"
848+
fullWidth
849+
validate={required()}
850+
>
851+
<SimpleFormIterator>
852+
<TextInput source="name" />
853+
</SimpleFormIterator>
854+
</ArrayInput>
855+
) : null}
856+
</SimpleForm>
857+
</Create>
858+
);
859+
};
860+
861+
export const ConditionalMountOnChangeValidation = () => (
862+
<TestMemoryRouter initialEntries={['/books/create']}>
863+
<Admin dataProvider={dataProvider}>
864+
<Resource name="books" create={BookCreateConditionalValidation} />
865+
</Admin>
866+
</TestMemoryRouter>
867+
);
868+
828869
const CreateGlobalValidationInFormTab = () => {
829870
return (
830871
<Create

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

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
useThemeProps,
1919
} from '@mui/material';
2020
import get from 'lodash/get.js';
21+
import isEqual from 'lodash/isEqual.js';
2122

2223
import { LinearProgress } from '../../layout/LinearProgress';
2324
import { InputHelperText } from '../InputHelperText';
@@ -88,18 +89,35 @@ export const ArrayInput = (inProps: ArrayInputProps) => {
8889
const parentSourceContext = useSourceContext();
8990
const finalSource = parentSourceContext.getSource(arraySource);
9091
const { subscribe } = useFormContext();
91-
92-
const [error, setError] = React.useState<any>();
92+
const [displayedError, setDisplayedError] = React.useState<any>();
9393
React.useEffect(() => {
9494
return subscribe({
95-
formState: { errors: true },
96-
callback: ({ errors }) => {
95+
formState: {
96+
dirtyFields: true,
97+
errors: true,
98+
isSubmitted: true,
99+
touchedFields: true,
100+
},
101+
callback: ({ dirtyFields, errors, isSubmitted, touchedFields }) => {
97102
const error = get(errors ?? {}, finalSource);
98-
setError(error);
103+
const hasBeenInteractedWith =
104+
get(dirtyFields ?? {}, finalSource, false) !== false ||
105+
get(touchedFields ?? {}, finalSource, false) !== false;
106+
const nextDisplayedError =
107+
hasBeenInteractedWith || isSubmitted ? error : undefined;
108+
109+
setDisplayedError(previousError =>
110+
isEqual(previousError, nextDisplayedError)
111+
? previousError
112+
: nextDisplayedError
113+
);
99114
},
100115
});
101116
}, [finalSource, subscribe]);
102-
const renderHelperText = helperText !== false || !!error;
117+
118+
const displayedErrorMessage = (displayedError?.root?.message ??
119+
displayedError?.message) as any;
120+
const renderHelperText = helperText !== false || !!displayedError;
103121

104122
if (isPending) {
105123
// We handle the loading state here instead of using the loading prop
@@ -121,14 +139,14 @@ export const ArrayInput = (inProps: ArrayInputProps) => {
121139
ArrayInputClasses.root,
122140
className
123141
)}
124-
error={!!error}
142+
error={!!displayedError}
125143
{...sanitizeInputRestProps(rest)}
126144
>
127145
<InputLabel
128146
component="span"
129147
className={ArrayInputClasses.label}
130148
shrink
131-
error={!!error}
149+
error={!!displayedError}
132150
>
133151
<FieldTitle
134152
label={label}
@@ -146,12 +164,12 @@ export const ArrayInput = (inProps: ArrayInputProps) => {
146164
{children}
147165
</ArrayInputBase>
148166
{renderHelperText ? (
149-
<FormHelperText error={!!error}>
167+
<FormHelperText error={!!displayedError}>
150168
<InputHelperText
151169
// root property is applicable to built-in validation only,
152170
// Resolvers are yet to support useFieldArray root level validation.
153171
// Reference: https://react-hook-form.com/docs/usefieldarray
154-
error={error?.root?.message ?? error?.message}
172+
error={displayedErrorMessage}
155173
helperText={helperText}
156174
/>
157175
</FormHelperText>

0 commit comments

Comments
 (0)