Skip to content

Commit

Permalink
Merge pull request #9723 from marmelab/warnwhenunsavedchanges
Browse files Browse the repository at this point in the history
Fix react-admin requires custom routers to be data routers
  • Loading branch information
slax57 authored Mar 15, 2024
2 parents 465bc6f + aee15f8 commit 8a0c18b
Show file tree
Hide file tree
Showing 14 changed files with 146 additions and 40 deletions.
2 changes: 1 addition & 1 deletion docs/AccordionForm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/EditTutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/Form.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/LongForm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

## `<LongForm.Section>`

Expand Down
2 changes: 1 addition & 1 deletion docs/SimpleForm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/TabbedForm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

## `<TabbedForm.Tab>`

Expand Down
24 changes: 12 additions & 12 deletions docs/Upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.

## `<Admin history>` Prop Was Removed

The `<Admin history>` prop was deprecated since version 4. It is no longer supported.
Expand Down
2 changes: 1 addition & 1 deletion docs/WizardForm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

## `<WizardForm.Step>`

Expand Down
12 changes: 12 additions & 0 deletions packages/ra-core/src/form/Form.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ZodResolver,
SanitizeEmptyValues,
NullValue,
InNonDataRouter,
} from './Form.stories';
import { mergeTranslations } from '../i18n';

Expand Down Expand Up @@ -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(<InNonDataRouter />);
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');
});
});
47 changes: 47 additions & 0 deletions packages/ra-core/src/form/Form.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -290,3 +291,49 @@ export const ZodResolver = ({
</CoreAdminContext>
);
};

const FormUnderTest = () => {
const navigate = useNavigate();
return (
<>
<Form
record={{ title: 'lorem', body: 'ipsum' }}
onSubmit={() => setTimeout(() => navigate('/'), 0)}
warnWhenUnsavedChanges
>
<Input source="title" />
<Input source="body" />
<button type="submit">Submit</button>
</Form>
<Link to="/">Leave the form</Link>
</>
);
};

export const WarnWhenUnsavedChanges = ({
i18nProvider = defaultI18nProvider,
}: {
i18nProvider?: I18nProvider;
}) => (
<CoreAdminContext i18nProvider={i18nProvider}>
<Routes>
<Route path="/" element={<Link to="/form">Go to form</Link>} />
<Route path="/form" element={<FormUnderTest />} />
</Routes>
</CoreAdminContext>
);

export const InNonDataRouter = ({
i18nProvider = defaultI18nProvider,
}: {
i18nProvider?: I18nProvider;
}) => (
<HashRouter>
<CoreAdminContext i18nProvider={i18nProvider}>
<Routes>
<Route path="/" element={<Link to="/form">Go to form</Link>} />
<Route path="/form" element={<FormUnderTest />} />
</Routes>
</CoreAdminContext>
</HashRouter>
);
68 changes: 57 additions & 11 deletions packages/ra-core/src/form/Form.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

/**
Expand Down Expand Up @@ -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 = <RecordType = any>(props: FormProps<RecordType>) => {
const { children, id, className, noValidate = false } = props;
export function Form<RecordType = any>(props: FormProps<RecordType>) {
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<SourceContextValue>(() => ({
getSource: (source: string) => source,
getLabel: (source: string) => `resources.${resource}.fields.${source}`,
}), [resource]);
const sourceContext = React.useMemo<SourceContextValue>(
() => ({
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 (
<OptionalRecordContextProvider value={record}>
Expand All @@ -70,17 +102,31 @@ export const Form = <RecordType = any>(props: FormProps<RecordType>) => {
>
{children}
</form>
{warnWhenUnsavedChanges &&
dataRouterContext &&
dataRouterStateContext && (
<WarnWhenUnsavedChangesComponent
enable
formRootPathName={formRootPathname}
formControl={form.control}
/>
)}
</FormGroupsProvider>
</FormProvider>
</SourceContextProvider>
</OptionalRecordContextProvider>
);
};
}

export type FormProps<RecordType = any> = FormOwnProps<RecordType> &
Omit<UseFormProps, 'onSubmit'> & {
validate?: ValidateForm;
noValidate?: boolean;
WarnWhenUnsavedChangesComponent?: React.ComponentType<{
enable?: boolean;
formRootPathName?: string;
formControl?: any;
}>;
};

export interface FormOwnProps<RecordType = any> {
Expand Down
10 changes: 10 additions & 0 deletions packages/ra-core/src/form/WarnWhenUnsavedChanges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useWarnWhenUnsavedChanges } from './useWarnWhenUnsavedChanges';

export const WarnWhenUnsavedChanges = ({
enable = true,
formRootPathName,
formControl,
}) => {
useWarnWhenUnsavedChanges(enable, formRootPathName, formControl);
return null;
};
1 change: 1 addition & 0 deletions packages/ra-core/src/form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ export * from './useInput';
export * from './useSuggestions';
export * from './useUnique';
export * from './useWarnWhenUnsavedChanges';
export * from './WarnWhenUnsavedChanges';
10 changes: 0 additions & 10 deletions packages/ra-core/src/form/useAugmentedForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -41,7 +40,6 @@ export const useAugmentedForm = <RecordType = any>(
reValidateMode = 'onChange',
onSubmit,
sanitizeEmptyValues,
warnWhenUnsavedChanges,
validate,
disableInvalidFormNotification,
...rest
Expand Down Expand Up @@ -83,13 +81,6 @@ export const useAugmentedForm = <RecordType = any>(
// 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) => {
Expand Down Expand Up @@ -141,7 +132,6 @@ export interface UseFormOwnProps<RecordType = any> {
formRootPathname?: string;
record?: Partial<RaRecord>;
onSubmit?: SubmitHandler<FieldValues> | SaveHandler<RecordType>;
warnWhenUnsavedChanges?: boolean;
sanitizeEmptyValues?: boolean;
disableInvalidFormNotification?: boolean;
}

0 comments on commit 8a0c18b

Please sign in to comment.