Skip to content
Open
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/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"react": "^18.3.1",
"react-admin": "^5.12.0",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-hook-form": "^7.65.0",
"react-router": "^6.28.1",
"react-router-dom": "^6.28.1"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/ra-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"jscodeshift": "^0.15.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-hook-form": "^7.65.0",
"react-router": "^6.28.1",
"react-router-dom": "^6.28.1",
"rimraf": "^3.0.2",
Expand All @@ -54,7 +54,7 @@
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"react-hook-form": "^7.53.0",
"react-hook-form": "^7.65.0",
"react-router": "^6.28.1 || ^7.1.1",
"react-router-dom": "^6.28.1 || ^7.1.1"
},
Expand Down
1 change: 1 addition & 0 deletions packages/ra-core/src/core/SourceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type SourceContextValue = {
export const SourceContext = createContext<SourceContextValue | undefined>(
undefined
);
SourceContext.displayName = 'SourceContext';

const defaultContextValue = {
getSource: (source: string) => source,
Expand Down
13 changes: 5 additions & 8 deletions packages/ra-core/src/form/FilterLiveForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,7 @@ export const FilterLiveForm = (props: FilterLiveFormProps) => {
resolver: finalResolver,
...rest,
});
const { handleSubmit, getValues, reset, watch, formState } = formContext;
const { isValid } = formState;
const { handleSubmit, getValues, reset, trigger, watch } = formContext;

const hasJustBeenModifiedByUser = React.useRef(false);

Expand All @@ -103,17 +102,15 @@ export const FilterLiveForm = (props: FilterLiveFormProps) => {
}, [JSON.stringify(filterValues), getValues, reset]);

const onSubmit = (values: any): void => {
// Do not call setFilters if the form is invalid
if (!isValid) {
return;
}
setFilters(mergeObjNotArray(filterValues, values));
};
const debouncedOnSubmit = useDebouncedEvent(onSubmit, debounce || 0);

// Submit the form on values change
useEffect(() => {
const { unsubscribe } = watch((values, { name }) => {
const { unsubscribe } = watch(async (values, { name }) => {
// Trigger validation manually
if (!(await trigger())) return;
Comment on lines +112 to +113
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure we already went through this issue, but apparently we did not cover it with a unit test (and apparently we should!): form validity has to be checked when the form is actually submitted. Checking it before triggering the debounced submission is not enough, because it doesn't work in the following scenario:

  1. Change the filters with a valid value
  2. A debounced form submission is triggered
  3. Change the filters to an invalid value
  4. The debounce delay expires
  5. The form is submitted with invalid values

// Check that the name is present to avoid setting filters when
// watch was triggered by a reset
if (name) {
Expand All @@ -129,7 +126,7 @@ export const FilterLiveForm = (props: FilterLiveFormProps) => {
}
});
return () => unsubscribe();
}, [watch, debouncedOnSubmit]);
}, [watch, debouncedOnSubmit, trigger]);

const sourceContext = React.useMemo<SourceContextValue>(
() => ({
Expand Down
1 change: 1 addition & 0 deletions packages/ra-core/src/form/groups/FormGroupsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createContext } from 'react';
export const FormGroupsContext = createContext<
FormGroupsContextValue | undefined
>(undefined);
FormGroupsContext.displayName = 'FormGroupsContext';

export type FormGroupSubscriber = () => void;

Expand Down
19 changes: 14 additions & 5 deletions packages/ra-core/src/form/useApplyInputDefaultValues.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import {
FieldValues,
UseFieldArrayReturn,
Expand Down Expand Up @@ -36,12 +36,21 @@ export const useApplyInputDefaultValues = ({
const finalSource = useWrappedSource(source);

const record = useRecordContext(inputProps);
const { getValues, resetField, formState, reset } = useFormContext();
const { getValues, resetField, reset, subscribe } = useFormContext();
const recordValue = get(record, finalSource);
const formValue = get(getValues(), finalSource);
const { dirtyFields } = formState;
const isDirty = Object.keys(dirtyFields).includes(finalSource);
const isDirty = useRef<boolean | undefined>(undefined);

useEffect(() => {
return subscribe({
formState: { dirtyFields: true },
callback: ({ dirtyFields }) => {
isDirty.current = Object.keys(dirtyFields ?? {}).includes(
finalSource
);
},
Comment on lines +47 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we say in #10997 that dirtyFields can also be a scalar value or an array of scalars?
And also that the presence of the field is not enough but it must be compared to true?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but #10997 is not merged yet and the RHF version we currently use in next does not have this issue afaik. We should probably wait for #10997 to be released though and reuse the dirtycheck logic that has been extracted

});
}, [finalSource, subscribe]);
useEffect(() => {
if (
defaultValue == null ||
Expand All @@ -52,7 +61,7 @@ export const useApplyInputDefaultValues = ({
// We check strictly for undefined to avoid setting default value
// when the field is null
recordValue !== undefined ||
isDirty
isDirty.current === true
) {
return;
}
Expand Down
8 changes: 6 additions & 2 deletions packages/ra-core/src/form/useAugmentedForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,24 @@ export const useAugmentedForm = <RecordType = any>(

const form = useForm({
criteriaMode,
values: defaultValuesIncludingRecord,
defaultValues: defaultValuesIncludingRecord,
reValidateMode,
resolver: finalResolver,
...rest,
});

const formRef = useRef(form);
const { reset } = form;

useEffect(() => {
reset(defaultValuesIncludingRecord);
}, [defaultValuesIncludingRecord, reset]);

// notify on invalid form
useNotifyIsFormInvalid(form.control, !disableInvalidFormNotification);

const recordFromLocation = useRecordFromLocation();
const recordFromLocationApplied = useRef(false);
const { reset } = form;
useEffect(() => {
if (recordFromLocation && !recordFromLocationApplied.current) {
reset(merge({}, defaultValuesIncludingRecord, recordFromLocation), {
Expand Down
45 changes: 42 additions & 3 deletions packages/ra-core/src/form/useInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { useForm, FormProvider, useFieldArray } from 'react-hook-form';
import { CoreAdminContext } from '../core';
import { Form } from './Form';
import { InputProps, useInput } from './useInput';
Expand All @@ -7,12 +8,15 @@ export default {
title: 'ra-core/form/useInput',
};

const Input = (props: InputProps) => {
const Input = (props: InputProps & { log?: boolean }) => {
const { label, log } = props;
const { id, field, fieldState } = useInput(props);

if (log) {
console.log(`Input ${id} rendered:`);
}
return (
<label htmlFor={id}>
{id}: <input id={id} {...field} />
{label ?? id}: <input id={id} {...field} />
{fieldState.error && <span>{fieldState.error.message}</span>}
</label>
);
Expand Down Expand Up @@ -86,3 +90,38 @@ DefaultValue.argTypes = {
control: { type: 'select' },
},
};

export const Large = () => {
const [submittedData, setSubmittedData] = React.useState<any>();
const fields = Array.from({ length: 15 }).map((_, index) => (
<Input
key={index}
source={`field${index + 1}`}
label={`field${index + 1}`}
/>
));
return (
<CoreAdminContext>
<Form
onSubmit={data => setSubmittedData(data)}
record={Array.from({ length: 15 }).reduce((acc, _, index) => {
acc[`field${index + 1}`] = `value${index + 1}`;
return acc;
}, {})}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '1em',
marginBottom: '1em',
}}
>
{fields}
</div>
<button type="submit">Submit</button>
</Form>
<pre>{JSON.stringify(submittedData, null, 2)}</pre>
</CoreAdminContext>
);
};
2 changes: 1 addition & 1 deletion packages/ra-input-rich-text/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"ra-ui-materialui": "^5.12.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-hook-form": "^7.65.0",
"rimraf": "^3.0.2",
"tippy.js": "^6.3.7",
"typescript": "^5.1.3"
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-ui-materialui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"ra-language-english": "^5.12.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-hook-form": "^7.65.0",
"react-is": "^18.2.0 || ^19.0.0",
"react-router": "^6.28.1",
"react-router-dom": "^6.28.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,44 @@ export const ReadOnly = () => (
</TestMemoryRouter>
);

export const DefaultValues = () => (
<TestMemoryRouter initialEntries={['/books/1']}>
<Admin dataProvider={dataProvider}>
<Resource
name="books"
edit={() => {
return (
<Edit
mutationMode="pessimistic"
mutationOptions={{
onSuccess: data => {
console.log(data);
},
}}
>
<SimpleForm>
<TextInput source="title" />
<ArrayInput source="authors">
<SimpleFormIterator>
<TextInput
source="name"
defaultValue="John Doe"
/>
<TextInput
source="role"
defaultValue="Author"
/>
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</Edit>
Comment on lines +176 to +206
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a weird issue while testing this story:
If you empty the "name" TextInput, then the default value "John Doe" gets reapplied immediately.

However it seems this is already the case on master!

So, this is not a regression from this PR, but this is something we should fix IMHO, either in this PR on in another one. Wdyt?

);
}}
/>
</Admin>
</TestMemoryRouter>
);

const BookEditWithAutocomplete = () => {
return (
<Edit
Expand Down
18 changes: 15 additions & 3 deletions packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import {
type ComponentsOverrides,
useThemeProps,
} from '@mui/material';
import { LinearProgress } from '../../layout';
import get from 'lodash/get';

import { LinearProgress } from '../../layout/LinearProgress';
import { InputHelperText } from '../InputHelperText';
import { sanitizeInputRestProps } from '../sanitizeInputRestProps';
import { Labeled } from '../../Labeled';
Expand Down Expand Up @@ -85,8 +87,18 @@ export const ArrayInput = (inProps: ArrayInputProps) => {

const parentSourceContext = useSourceContext();
const finalSource = parentSourceContext.getSource(arraySource);
const { getFieldState, formState } = useFormContext();
const { error } = getFieldState(finalSource, formState);
const { subscribe } = useFormContext();

const [error, setError] = React.useState<any>();
React.useEffect(() => {
return subscribe({
formState: { errors: true },
callback: ({ errors }) => {
const error = get(errors ?? {}, finalSource);
setError(error);
},
});
}, [finalSource, subscribe]);
const renderHelperText = helperText !== false || !!error;

if (isPending) {
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lastly, I tried building the EE packages with this version and ran into a TS issue.
Actually, the issue was not brought by this PR but by #10955.

Still, I figured you may want to fix it in this PR, before the 5.13.0 is released.

PR #10955 added a clear callback to the SimpleFormIteratorContext, but did not mark the field as optional. So this may break TS compilation for users providing their own SimpleFormIteratorContext, which is the case of SimpleFormIterator in test-ui.
We need to mark this field as optional IMO.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Edit } from '../../detail';
import { SimpleForm } from '../../form';
import { ArrayInput } from './ArrayInput';
import { SimpleFormIterator } from './SimpleFormIterator';
import { NumberInput } from '../NumberInput';
import { TextInput } from '../TextInput';
import { AdminContext } from '../../AdminContext';
import { defaultTheme } from '../../theme/defaultTheme';
Expand All @@ -14,6 +15,7 @@ import {
testDataProvider,
useSimpleFormIteratorItem,
} from 'ra-core';
import { AutocompleteInput } from '../AutocompleteInput';

export default { title: 'ra-ui-materialui/input/SimpleFormIterator' };

Expand Down Expand Up @@ -286,3 +288,43 @@ export const WithFormDataConsumer = () => (
</ResourceContextProvider>
</AdminContext>
);

const largeDataProvider = {
getOne: async () => ({
data: {
id: 1,
name: 'Book 1',
authors: Array.from({ length: 100 }, (_, i) => ({
id: i + 1,
first_name: `Author ${i + 1}`,
last_name: `LastName ${i + 1}`,
age: 30 + (i % 20),
})),
},
}),
} as any;

export const Large = () => (
<AdminContext dataProvider={largeDataProvider} defaultTheme="light">
<Edit resource="books" id="1">
<SimpleForm>
<TextInput source="name" />
<ArrayInput source="authors">
<SimpleFormIterator inline>
<TextInput source="first_name" helperText={false} />
<TextInput source="last_name" helperText={false} />
<NumberInput source="age" helperText={false} />
<AutocompleteInput
source="status"
choices={[
{ id: 'active', name: 'Active' },
{ id: 'inactive', name: 'Inactive' },
]}
helperText={false}
/>
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</Edit>
</AdminContext>
);
Loading
Loading