Skip to content

Commit 4dcdbdf

Browse files
committed
Fix ArrayInput makes the form dirty in strict mode
1 parent d135bd3 commit 4dcdbdf

File tree

3 files changed

+98
-96
lines changed

3 files changed

+98
-96
lines changed

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

Lines changed: 11 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import userEvent from '@testing-library/user-event';
44
import {
55
RecordContextProvider,
66
ResourceContextProvider,
7-
minLength,
8-
required,
97
testDataProvider,
108
} from 'ra-core';
119

@@ -23,6 +21,7 @@ import {
2321
NestedInline,
2422
WithReferenceField,
2523
NestedInlineNoTranslation,
24+
Validation,
2625
} from './ArrayInput.stories';
2726
import { useArrayInput } from './useArrayInput';
2827

@@ -136,66 +135,19 @@ describe('<ArrayInput />', () => {
136135
});
137136

138137
it('should apply validation to both itself and its inner inputs', async () => {
139-
render(
140-
<AdminContext dataProvider={testDataProvider()}>
141-
<ResourceContextProvider value="bar">
142-
<SimpleForm
143-
onSubmit={jest.fn}
144-
defaultValues={{
145-
arr: [],
146-
}}
147-
>
148-
<ArrayInput
149-
source="arr"
150-
validate={[minLength(2, 'array_min_length')]}
151-
>
152-
<SimpleFormIterator>
153-
<TextInput
154-
source="id"
155-
validate={[required('id_required')]}
156-
/>
157-
<TextInput
158-
source="foo"
159-
validate={[required('foo_required')]}
160-
/>
161-
</SimpleFormIterator>
162-
</ArrayInput>
163-
</SimpleForm>
164-
</ResourceContextProvider>
165-
</AdminContext>
166-
);
138+
render(<Validation />);
167139

168-
fireEvent.click(screen.getByLabelText('ra.action.add'));
169-
fireEvent.click(screen.getByText('ra.action.save'));
170-
await waitFor(() => {
171-
expect(screen.queryByText('array_min_length')).not.toBeNull();
172-
});
173-
fireEvent.click(screen.getByLabelText('ra.action.add'));
174-
const firstId = screen.getAllByLabelText(
175-
'resources.bar.fields.arr.id *'
176-
)[0];
177-
fireEvent.change(firstId, {
178-
target: { value: 'aaa' },
179-
});
180-
fireEvent.change(firstId, {
181-
target: { value: '' },
182-
});
183-
fireEvent.blur(firstId);
184-
const firstFoo = screen.getAllByLabelText(
185-
'resources.bar.fields.arr.foo *'
186-
)[0];
187-
fireEvent.change(firstFoo, {
188-
target: { value: 'aaa' },
189-
});
190-
fireEvent.change(firstFoo, {
191-
target: { value: '' },
192-
});
193-
fireEvent.blur(firstFoo);
194-
expect(screen.queryByText('array_min_length')).toBeNull();
140+
fireEvent.click(await screen.findByLabelText('Add'));
141+
fireEvent.click(screen.getByText('Save'));
195142
await waitFor(() => {
196-
expect(screen.queryByText('id_required')).not.toBeNull();
197-
expect(screen.queryByText('foo_required')).not.toBeNull();
143+
// The two inputs in each item are required
144+
expect(screen.queryAllByText('Required')).toHaveLength(2);
198145
});
146+
fireEvent.click(screen.getAllByLabelText('Remove')[2]);
147+
fireEvent.click(screen.getAllByLabelText('Remove')[1]);
148+
fireEvent.click(screen.getByText('Save'));
149+
150+
await screen.findByText('You need two authors at minimum');
199151
});
200152

201153
it('should maintain its form value after having been unmounted', async () => {

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

Lines changed: 81 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
22
import { Admin } from 'react-admin';
33
import {
4+
minLength,
45
required,
56
Resource,
67
testI18nProvider,
@@ -19,6 +20,7 @@ import { AutocompleteInput } from '../AutocompleteInput';
1920
import { TranslatableInputs } from '../TranslatableInputs';
2021
import { ReferenceField, TextField, TranslatableFields } from '../../field';
2122
import { Labeled } from '../../Labeled';
23+
import { useFormContext, useWatch } from 'react-hook-form';
2224

2325
export default { title: 'ra-ui-materialui/input/ArrayInput' };
2426

@@ -60,6 +62,7 @@ const BookEdit = () => {
6062
}}
6163
>
6264
<SimpleForm>
65+
<FormInspector />
6366
<TextInput source="title" />
6467
<ArrayInput source="authors">
6568
<SimpleFormIterator>
@@ -72,12 +75,28 @@ const BookEdit = () => {
7275
);
7376
};
7477

78+
const FormInspector = () => {
79+
const {
80+
formState: { defaultValues, isDirty, dirtyFields },
81+
} = useFormContext();
82+
const values = useWatch();
83+
return (
84+
<div>
85+
<div>isDirty: {isDirty.toString()}</div>
86+
<div>dirtyFields: {JSON.stringify(dirtyFields, null, 2)}</div>
87+
<div>defaultValues: {JSON.stringify(defaultValues, null, 2)}</div>
88+
<div>values: {JSON.stringify(values, null, 2)}</div>
89+
</div>
90+
);
91+
};
7592
export const Basic = () => (
76-
<TestMemoryRouter initialEntries={['/books/1']}>
77-
<Admin dataProvider={dataProvider}>
78-
<Resource name="books" edit={BookEdit} />
79-
</Admin>
80-
</TestMemoryRouter>
93+
<React.StrictMode>
94+
<TestMemoryRouter initialEntries={['/books/1']}>
95+
<Admin dataProvider={dataProvider}>
96+
<Resource name="books" edit={BookEdit} />
97+
</Admin>
98+
</TestMemoryRouter>
99+
</React.StrictMode>
81100
);
82101

83102
export const Disabled = () => (
@@ -669,24 +688,44 @@ export const ActionsLeft = () => (
669688
</TestMemoryRouter>
670689
);
671690

672-
const globalValidator = values => {
673-
const errors: any = {};
674-
if (!values.authors || !values.authors.length) {
675-
errors.authors = 'ra.validation.required';
676-
} else {
677-
errors.authors = values.authors.map(author => {
678-
const authorErrors: any = {};
679-
if (!author?.name) {
680-
authorErrors.name = 'A name is required';
681-
}
682-
if (!author?.role) {
683-
authorErrors.role = 'ra.validation.required';
684-
}
685-
return authorErrors;
686-
});
687-
}
688-
return errors;
691+
const BookEditValidation = () => {
692+
return (
693+
<Edit
694+
mutationMode="pessimistic"
695+
mutationOptions={{
696+
onSuccess: data => {
697+
console.log(data);
698+
},
699+
}}
700+
>
701+
<SimpleForm>
702+
<ArrayInput
703+
source="authors"
704+
fullWidth
705+
validate={[
706+
required(),
707+
minLength(2, 'You need two authors at minimum'),
708+
]}
709+
helperText="At least two authors"
710+
>
711+
<SimpleFormIterator>
712+
<TextInput source="name" validate={required()} />
713+
<TextInput source="role" validate={required()} />
714+
</SimpleFormIterator>
715+
</ArrayInput>
716+
</SimpleForm>
717+
</Edit>
718+
);
689719
};
720+
721+
export const Validation = () => (
722+
<TestMemoryRouter initialEntries={['/books/1']}>
723+
<Admin dataProvider={dataProvider}>
724+
<Resource name="books" edit={BookEditValidation} />
725+
</Admin>
726+
</TestMemoryRouter>
727+
);
728+
690729
const BookEditGlobalValidation = () => {
691730
return (
692731
<Edit
@@ -712,6 +751,26 @@ const BookEditGlobalValidation = () => {
712751
</Edit>
713752
);
714753
};
754+
755+
const globalValidator = values => {
756+
const errors: any = {};
757+
if (!values.authors || !values.authors.length) {
758+
errors.authors = 'ra.validation.required';
759+
} else {
760+
errors.authors = values.authors.map(author => {
761+
const authorErrors: any = {};
762+
if (!author?.name) {
763+
authorErrors.name = 'A name is required';
764+
}
765+
if (!author?.role) {
766+
authorErrors.role = 'ra.validation.required';
767+
}
768+
return authorErrors;
769+
});
770+
}
771+
return errors;
772+
};
773+
715774
export const GlobalValidation = () => (
716775
<TestMemoryRouter initialEntries={['/books/1']}>
717776
<Admin dataProvider={dataProvider}>

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

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,7 @@ export const ArrayInput = (props: ArrayInputProps) => {
101101
: validate;
102102
const getValidationErrorMessage = useGetValidationErrorMessage();
103103

104-
const { getFieldState, formState, getValues, register, unregister } =
105-
useFormContext();
104+
const { getFieldState, formState, getValues } = useFormContext();
106105

107106
const fieldProps = useFieldArray({
108107
name: finalSource,
@@ -121,25 +120,17 @@ export const ArrayInput = (props: ArrayInputProps) => {
121120
},
122121
});
123122

124-
// We need to register the array itself as a field to enable validation at its level
125123
useEffect(() => {
126-
register(finalSource);
127-
formGroups &&
128-
formGroupName != null &&
124+
if (formGroups && formGroupName != null) {
129125
formGroups.registerField(finalSource, formGroupName);
126+
}
130127

131128
return () => {
132-
unregister(finalSource, {
133-
keepValue: true,
134-
keepError: true,
135-
keepDirty: true,
136-
keepTouched: true,
137-
});
138-
formGroups &&
139-
formGroupName != null &&
129+
if (formGroups && formGroupName != null) {
140130
formGroups.unregisterField(finalSource, formGroupName);
131+
}
141132
};
142-
}, [register, unregister, finalSource, formGroups, formGroupName]);
133+
}, [finalSource, formGroups, formGroupName]);
143134

144135
useApplyInputDefaultValues({
145136
inputProps: props,

0 commit comments

Comments
 (0)