Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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.3",
"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 @@ -48,7 +48,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",
"typescript": "^5.1.3",
Expand All @@ -59,7 +59,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
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const SimpleFormIteratorBase = (props: SimpleFormIteratorBaseProps) => {
);
}

const { append, fields, move, remove, replace } = useArrayInput(props);
const { append, fields, move, remove } = useArrayInput(props);
const { trigger, getValues } = useFormContext();

const removeField = useEvent((index: number) => {
Expand All @@ -47,7 +47,7 @@ export const SimpleFormIteratorBase = (props: SimpleFormIteratorBaseProps) => {
});

const handleArrayClear = useEvent(() => {
replace([]);
remove();
});

const context = useMemo(
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
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({
// Even though we only need dirtyFields, we subscribe to values as well to
// ensure we properly receive dirtyFields updates for newly added items in an ArrayInput
formState: { values: true, dirtyFields: true },
callback: ({ dirtyFields }) => {
isDirty.current = get(dirtyFields ?? {}, finalSource, false);
},
});
}, [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';

Check warning on line 2 in packages/ra-core/src/form/useInput.stories.tsx

View workflow job for this annotation

GitHub Actions / typecheck

'useFieldArray' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 2 in packages/ra-core/src/form/useInput.stories.tsx

View workflow job for this annotation

GitHub Actions / typecheck

'FormProvider' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 2 in packages/ra-core/src/form/useInput.stories.tsx

View workflow job for this annotation

GitHub Actions / typecheck

'useForm' is defined but never used. Allowed unused vars must match /^_/u
import { CoreAdminContext } from '../core';
import { Form } from './Form';
import { InputProps, useInput } from './useInput';
Expand All @@ -7,12 +8,15 @@
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 @@
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>
);
};
6 changes: 3 additions & 3 deletions packages/ra-core/src/test-ui/SimpleFormIterator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,13 +199,13 @@ export const SimpleFormIterator = (props: SimpleFormIteratorProps) => {
}

const [confirmIsOpen, setConfirmIsOpen] = useState<boolean>(false);
const { fields, replace } = useArrayInput(props);
const { fields, remove } = useArrayInput(props);
const translate = useTranslate();

const handleArrayClear = useCallback(() => {
replace([]);
remove();
setConfirmIsOpen(false);
}, [replace]);
}, [remove]);

const records = useFieldValue({ source: finalSource });

Expand Down
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 @@ -56,7 +56,7 @@
"ra-ui-materialui": "^5.12.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-hook-form": "^7.65.0",
"tippy.js": "^6.3.7",
"typescript": "^5.1.3",
"zshy": "^0.4.4"
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 @@ -46,7 +46,7 @@
"ra-language-english": "^5.12.3",
"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>
);
}}
/>
</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.js';

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
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