diff --git a/docs/AccordionForm.md b/docs/AccordionForm.md index e8590a66a1b..2aafb8940b3 100644 --- a/docs/AccordionForm.md +++ b/docs/AccordionForm.md @@ -351,7 +351,7 @@ export const TagEdit = () => ( ); ``` -**Warning**: This feature only works if you have a dependency on react-router 6.3.0 **at most**. The react-router team disabled this possibility in react-router 6.4, so `warnWhenUnsavedChanges` will silently fail with react-router 6.4 or later. +**Note**: Due to limitations in react-router, this feature only works if you use the default router provided by react-admin, or if you use a [Data Router](https://reactrouter.com/en/6.22.3/routers/picking-a-router). ## `sx`: CSS API diff --git a/docs/EditTutorial.md b/docs/EditTutorial.md index 861b22bcd2f..7c60576deeb 100644 --- a/docs/EditTutorial.md +++ b/docs/EditTutorial.md @@ -683,7 +683,7 @@ const Form = ({ onSubmit }) => { **Tip**: You can customize the message displayed in the confirm dialog by setting the `ra.message.unsaved_changes` message in your i18nProvider. -**Warning**: This feature only works if you have a dependency on react-router 6.3.0 **at most**. The react-router team disabled this possibility in react-router 6.4, so `warnWhenUnsavedChanges` will silently fail with react-router 6.4 or later. +**Note**: Due to limitations in react-router, this feature only works if you use the default router provided by react-admin, or if you use a [Data Router](https://reactrouter.com/en/6.22.3/routers/picking-a-router). ## Submit On Enter diff --git a/docs/Form.md b/docs/Form.md index c1cd3054a08..9e0d7e16143 100644 --- a/docs/Form.md +++ b/docs/Form.md @@ -236,7 +236,7 @@ export const TagEdit = () => ( ); ``` -**Warning**: This feature only works if you have a dependency on react-router 6.3.0 **at most**. The react-router team disabled this possibility in react-router 6.4, so `warnWhenUnsavedChanges` will silently fail with react-router 6.4 or later. +**Note**: Due to limitations in react-router, this feature only works if you use the default router provided by react-admin, or if you use a [Data Router](https://reactrouter.com/en/6.22.3/routers/picking-a-router). ## Subscribing To Form Changes diff --git a/docs/LongForm.md b/docs/LongForm.md index ca93185f270..3f50c3d957a 100644 --- a/docs/LongForm.md +++ b/docs/LongForm.md @@ -349,7 +349,7 @@ export const TagEdit = () => ( ); ``` -**Warning**: This feature only works if you have a dependency on react-router 6.3.0 **at most**. The react-router team disabled this possibility in react-router 6.4, so `warnWhenUnsavedChanges` will silently fail with react-router 6.4 or later. +**Note**: Due to limitations in react-router, this feature only works if you use the default router provided by react-admin, or if you use a [Data Router](https://reactrouter.com/en/6.22.3/routers/picking-a-router). ## `` diff --git a/docs/SimpleForm.md b/docs/SimpleForm.md index 2ac68a3c62f..e21aa859bbb 100644 --- a/docs/SimpleForm.md +++ b/docs/SimpleForm.md @@ -400,7 +400,7 @@ export const TagEdit = () => ( ); ``` -**Warning**: This feature only works if you have a dependency on react-router 6.3.0 **at most**. The react-router team disabled this possibility in react-router 6.4, so `warnWhenUnsavedChanges` will silently fail with react-router 6.4 or later. +**Note**: Due to limitations in react-router, this feature only works if you use the default router provided by react-admin, or if you use a [Data Router](https://reactrouter.com/en/6.22.3/routers/picking-a-router). ## Using Fields As Children diff --git a/docs/TabbedForm.md b/docs/TabbedForm.md index 86d48ed1a92..6fe641067e7 100644 --- a/docs/TabbedForm.md +++ b/docs/TabbedForm.md @@ -500,7 +500,7 @@ export const TagEdit = () => ( ); ``` -**Warning**: This feature only works if you have a dependency on react-router 6.3.0 **at most**. The react-router team disabled this possibility in react-router 6.4, so `warnWhenUnsavedChanges` will silently fail with react-router 6.4 or later. +**Note**: Due to limitations in react-router, this feature only works if you use the default router provided by react-admin, or if you use a [Data Router](https://reactrouter.com/en/6.22.3/routers/picking-a-router). ## `` diff --git a/docs/Upgrade.md b/docs/Upgrade.md index d5e98e87d12..cb421c716fc 100644 --- a/docs/Upgrade.md +++ b/docs/Upgrade.md @@ -613,11 +613,20 @@ import { FieldProps, useRecordContext } from 'react-admin'; } ``` -## Custom Routers Must Be Upgraded To Data Routers +## `warnWhenUnsavedChanges` Changes -If you are using a [custom router](./Routing.md#using-a-custom-router), or if your React Admin app is embedded in another app with its own router, you will need to [migrate to a data router](https://reactrouter.com/en/main/upgrading/v6-data). This is because React Admin now uses `react-router`'s data router. +The `warnWhenUnsavedChanges` feature is a little more restrictive than before: -For instance, if you were using `react-router`'s `BrowserRouter`, you will need to migrate to `createBrowserRouter` and wrap your app in a `RouterProvider`: +- It will open a confirmation dialog (and block the navigation) if a navigation is fired when the form is currently submitting (submission will continue in the background). +- [Due to browser constraints](https://stackoverflow.com/questions/38879742/is-it-possible-to-display-a-custom-message-in-the-beforeunload-popup), the message displayed in the confirmation dialog when closing the browser's tab cannot be customized (it is managed by the browser). + +This behavior allows to prevent unwanted data loss in more situations. No changes are required in the code. + +However, the `warnWhenUnsavedChanges` now requires a [Data Router](https://reactrouter.com/en/6.22.3/routers/picking-a-router) (a new type of router from react-router) to work. React-admin uses such a data router by default, so the feature works out of the box in v5. + +However, if you use a [custom router](./Routing.md#using-a-custom-router) and the `warnWhenUnsavedChanges` prop, the "warn when unsaved changes" feature will be disabled. + +To re-enable it, you'll have to migrate your custom router to use the data router. For instance, if you were using `react-router`'s `BrowserRouter`, you will need to migrate to `createBrowserRouter` and wrap your app in a `RouterProvider`: ```diff import * as React from 'react'; @@ -652,15 +661,6 @@ root.render( **Tip:** Check out the [Migrating to RouterProvider](https://reactrouter.com/en/main/upgrading/v6-data) documentation to learn more about the migration steps and impacts. -## `warnWhenUnsavedChanges` Is More Restrictive - -Due to the migration to `react-router`'s data router, you will notice that the `useWarnWhenUnsavedChanges` hook is a little more restrictive than before. Here are the main changes: - -- `useWarnWhenUnsavedChanges` will also open a confirmation dialog (and block the navigation) if a navigation is fired when the form is currently submitting (submission will continue in the background). -- [Due to browser constraints](https://stackoverflow.com/questions/38879742/is-it-possible-to-display-a-custom-message-in-the-beforeunload-popup), the message displayed in the confirmation dialog when closing the browser's tab cannot be customized (it is managed by the browser). - -This behavior is a little more restrictive, but it allows to prevent unwanted data loss in most situations. No changes are required in the code. - ## `` Prop Was Removed The `` prop was deprecated since version 4. It is no longer supported. diff --git a/docs/WizardForm.md b/docs/WizardForm.md index 1094061ba70..069b1fc353f 100644 --- a/docs/WizardForm.md +++ b/docs/WizardForm.md @@ -404,7 +404,7 @@ export const TagEdit = () => ( ); ``` -**Warning**: This feature only works if you have a dependency on react-router 6.3.0 **at most**. The react-router team disabled this possibility in react-router 6.4, so `warnWhenUnsavedChanges` will silently fail with react-router 6.4 or later. +**Note**: Due to limitations in react-router, this feature only works if you use the default router provided by react-admin, or if you use a [Data Router](https://reactrouter.com/en/6.22.3/routers/picking-a-router). ## `` diff --git a/packages/ra-core/src/form/Form.spec.tsx b/packages/ra-core/src/form/Form.spec.tsx index 3df09b79079..487276ad643 100644 --- a/packages/ra-core/src/form/Form.spec.tsx +++ b/packages/ra-core/src/form/Form.spec.tsx @@ -18,6 +18,7 @@ import { ZodResolver, SanitizeEmptyValues, NullValue, + InNonDataRouter, } from './Form.stories'; import { mergeTranslations } from '../i18n'; @@ -756,4 +757,15 @@ describe('Form', () => { expect(translate).not.toHaveBeenCalledWith('This field is required'); mock.mockRestore(); }); + + it('should work even inside a non-data router', async () => { + render(); + fireEvent.click(screen.getByText('Go to form')); + await screen.findByText('title'); + fireEvent.change(screen.getByLabelText('title'), { + target: { value: '' }, + }); + fireEvent.click(screen.getByText('Leave the form')); + await screen.findByText('Go to form'); + }); }); diff --git a/packages/ra-core/src/form/Form.stories.tsx b/packages/ra-core/src/form/Form.stories.tsx index b5397e5cdb4..5d0115aa244 100644 --- a/packages/ra-core/src/form/Form.stories.tsx +++ b/packages/ra-core/src/form/Form.stories.tsx @@ -8,6 +8,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; import polyglotI18nProvider from 'ra-i18n-polyglot'; import englishMessages from 'ra-language-english'; +import { Route, Routes, useNavigate, Link, HashRouter } from 'react-router-dom'; import { CoreAdminContext } from '../core'; import { Form } from './Form'; @@ -290,3 +291,49 @@ export const ZodResolver = ({ ); }; + +const FormUnderTest = () => { + const navigate = useNavigate(); + return ( + <> +
setTimeout(() => navigate('/'), 0)} + warnWhenUnsavedChanges + > + + + +
+ Leave the form + + ); +}; + +export const WarnWhenUnsavedChanges = ({ + i18nProvider = defaultI18nProvider, +}: { + i18nProvider?: I18nProvider; +}) => ( + + + Go to form} /> + } /> + + +); + +export const InNonDataRouter = ({ + i18nProvider = defaultI18nProvider, +}: { + i18nProvider?: I18nProvider; +}) => ( + + + + Go to form} /> + } /> + + + +); diff --git a/packages/ra-core/src/form/Form.tsx b/packages/ra-core/src/form/Form.tsx index 5d440bf6d4a..a75fdf35932 100644 --- a/packages/ra-core/src/form/Form.tsx +++ b/packages/ra-core/src/form/Form.tsx @@ -1,11 +1,15 @@ import * as React from 'react'; -import { ReactNode } from 'react'; +import { ReactNode, useContext } from 'react'; import { FormProvider, FieldValues, UseFormProps, SubmitHandler, } from 'react-hook-form'; +import { + UNSAFE_DataRouterContext, + UNSAFE_DataRouterStateContext, +} from 'react-router'; import { FormGroupsProvider } from './FormGroupsProvider'; import { RaRecord } from '../types'; @@ -14,8 +18,13 @@ import { OptionalRecordContextProvider, SaveHandler, } from '../controller'; -import { SourceContextProvider, SourceContextValue, useResourceContext } from '../core'; +import { + SourceContextProvider, + SourceContextValue, + useResourceContext, +} from '../core'; import { ValidateForm } from './getSimpleValidationResolver'; +import { WarnWhenUnsavedChanges } from './WarnWhenUnsavedChanges'; import { useAugmentedForm } from './useAugmentedForm'; /** @@ -45,17 +54,40 @@ import { useAugmentedForm } from './useAugmentedForm'; * * @link https://react-hook-form.com/docs/useformcontext */ -// TODO: remove after upgrading prettier -// eslint-disable-next-line prettier/prettier -export const Form = (props: FormProps) => { - const { children, id, className, noValidate = false } = props; +export function Form(props: FormProps) { + const { + children, + id, + className, + noValidate = false, + formRootPathname, + warnWhenUnsavedChanges, + WarnWhenUnsavedChangesComponent = WarnWhenUnsavedChanges, + } = props; const record = useRecordContext(props); const resource = useResourceContext(props); const { form, formHandleSubmit } = useAugmentedForm(props); - const sourceContext = React.useMemo(() => ({ - getSource: (source: string) => source, - getLabel: (source: string) => `resources.${resource}.fields.${source}`, - }), [resource]); + const sourceContext = React.useMemo( + () => ({ + getSource: (source: string) => source, + getLabel: (source: string) => + `resources.${resource}.fields.${source}`, + }), + [resource] + ); + const dataRouterContext = useContext(UNSAFE_DataRouterContext); + const dataRouterStateContext = useContext(UNSAFE_DataRouterStateContext); + if ( + warnWhenUnsavedChanges && + (!dataRouterContext || !dataRouterStateContext) && + process.env.NODE_ENV === 'development' + ) { + console.error( + 'Cannot use the warnWhenUnsavedChanges feature outside of a DataRouter. ' + + 'The warnWhenUnsavedChanges feature is disabled. ' + + 'Remove the warnWhenUnsavedChanges prop or convert your custom router to a Data Router.' + ); + } return ( @@ -70,17 +102,31 @@ export const Form = (props: FormProps) => { > {children} + {warnWhenUnsavedChanges && + dataRouterContext && + dataRouterStateContext && ( + + )} ); -}; +} export type FormProps = FormOwnProps & Omit & { validate?: ValidateForm; noValidate?: boolean; + WarnWhenUnsavedChangesComponent?: React.ComponentType<{ + enable?: boolean; + formRootPathName?: string; + formControl?: any; + }>; }; export interface FormOwnProps { diff --git a/packages/ra-core/src/form/WarnWhenUnsavedChanges.ts b/packages/ra-core/src/form/WarnWhenUnsavedChanges.ts new file mode 100644 index 00000000000..f6c4a35dd4f --- /dev/null +++ b/packages/ra-core/src/form/WarnWhenUnsavedChanges.ts @@ -0,0 +1,10 @@ +import { useWarnWhenUnsavedChanges } from './useWarnWhenUnsavedChanges'; + +export const WarnWhenUnsavedChanges = ({ + enable = true, + formRootPathName, + formControl, +}) => { + useWarnWhenUnsavedChanges(enable, formRootPathName, formControl); + return null; +}; diff --git a/packages/ra-core/src/form/index.ts b/packages/ra-core/src/form/index.ts index cafbef164e1..eff073babf8 100644 --- a/packages/ra-core/src/form/index.ts +++ b/packages/ra-core/src/form/index.ts @@ -47,3 +47,4 @@ export * from './useInput'; export * from './useSuggestions'; export * from './useUnique'; export * from './useWarnWhenUnsavedChanges'; +export * from './WarnWhenUnsavedChanges'; diff --git a/packages/ra-core/src/form/useAugmentedForm.ts b/packages/ra-core/src/form/useAugmentedForm.ts index 9d489610ead..f6770c85b47 100644 --- a/packages/ra-core/src/form/useAugmentedForm.ts +++ b/packages/ra-core/src/form/useAugmentedForm.ts @@ -16,7 +16,6 @@ import { } from './getSimpleValidationResolver'; import { setSubmissionErrors } from './setSubmissionErrors'; import { useNotifyIsFormInvalid } from './useNotifyIsFormInvalid'; -import { useWarnWhenUnsavedChanges } from './useWarnWhenUnsavedChanges'; import { sanitizeEmptyValues as sanitizeValues } from './sanitizeEmptyValues'; /** @@ -41,7 +40,6 @@ export const useAugmentedForm = ( reValidateMode = 'onChange', onSubmit, sanitizeEmptyValues, - warnWhenUnsavedChanges, validate, disableInvalidFormNotification, ...rest @@ -83,13 +81,6 @@ export const useAugmentedForm = ( // notify on invalid form useNotifyIsFormInvalid(form.control, !disableInvalidFormNotification); - // warn when unsaved change - useWarnWhenUnsavedChanges( - Boolean(warnWhenUnsavedChanges), - formRootPathname, - form.control - ); - // submit callbacks const handleSubmit = useCallback( async (values, event) => { @@ -141,7 +132,6 @@ export interface UseFormOwnProps { formRootPathname?: string; record?: Partial; onSubmit?: SubmitHandler | SaveHandler; - warnWhenUnsavedChanges?: boolean; sanitizeEmptyValues?: boolean; disableInvalidFormNotification?: boolean; }