Skip to content

Commit cd440fd

Browse files
authored
Merge pull request marmelab#10421 from marmelab/fix-array-input-dirty
Fix ArrayInput makes the form dirty in strict mode
2 parents a1b7be0 + ce33414 commit cd440fd

File tree

3 files changed

+99
-96
lines changed

3 files changed

+99
-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: 82 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

@@ -67,17 +69,35 @@ const BookEdit = () => {
6769
<TextInput source="role" />
6870
</SimpleFormIterator>
6971
</ArrayInput>
72+
<FormInspector />
7073
</SimpleForm>
7174
</Edit>
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+
};
92+
7593
export const Basic = () => (
76-
<TestMemoryRouter initialEntries={['/books/1']}>
77-
<Admin dataProvider={dataProvider}>
78-
<Resource name="books" edit={BookEdit} />
79-
</Admin>
80-
</TestMemoryRouter>
94+
<React.StrictMode>
95+
<TestMemoryRouter initialEntries={['/books/1']}>
96+
<Admin dataProvider={dataProvider}>
97+
<Resource name="books" edit={BookEdit} />
98+
</Admin>
99+
</TestMemoryRouter>
100+
</React.StrictMode>
81101
);
82102

83103
export const Disabled = () => (
@@ -669,24 +689,44 @@ export const ActionsLeft = () => (
669689
</TestMemoryRouter>
670690
);
671691

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;
692+
const BookEditValidation = () => {
693+
return (
694+
<Edit
695+
mutationMode="pessimistic"
696+
mutationOptions={{
697+
onSuccess: data => {
698+
console.log(data);
699+
},
700+
}}
701+
>
702+
<SimpleForm>
703+
<ArrayInput
704+
source="authors"
705+
fullWidth
706+
validate={[
707+
required(),
708+
minLength(2, 'You need two authors at minimum'),
709+
]}
710+
helperText="At least two authors"
711+
>
712+
<SimpleFormIterator>
713+
<TextInput source="name" validate={required()} />
714+
<TextInput source="role" validate={required()} />
715+
</SimpleFormIterator>
716+
</ArrayInput>
717+
</SimpleForm>
718+
</Edit>
719+
);
689720
};
721+
722+
export const Validation = () => (
723+
<TestMemoryRouter initialEntries={['/books/1']}>
724+
<Admin dataProvider={dataProvider}>
725+
<Resource name="books" edit={BookEditValidation} />
726+
</Admin>
727+
</TestMemoryRouter>
728+
);
729+
690730
const BookEditGlobalValidation = () => {
691731
return (
692732
<Edit
@@ -712,6 +752,26 @@ const BookEditGlobalValidation = () => {
712752
</Edit>
713753
);
714754
};
755+
756+
const globalValidator = values => {
757+
const errors: any = {};
758+
if (!values.authors || !values.authors.length) {
759+
errors.authors = 'ra.validation.required';
760+
} else {
761+
errors.authors = values.authors.map(author => {
762+
const authorErrors: any = {};
763+
if (!author?.name) {
764+
authorErrors.name = 'A name is required';
765+
}
766+
if (!author?.role) {
767+
authorErrors.role = 'ra.validation.required';
768+
}
769+
return authorErrors;
770+
});
771+
}
772+
return errors;
773+
};
774+
715775
export const GlobalValidation = () => (
716776
<TestMemoryRouter initialEntries={['/books/1']}>
717777
<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)