New form layouts for complex data entry tasks (accordion, wizard, etc.).

Test it live on the Enterprise Edition Storybook and in the e-commerce demo (Accordion Form, WizardForm).

Installation

npm install --save @react-admin/ra-form-layout
# or
yarn add @react-admin/ra-form-layout

Tip: ra-form-layout is part of the React-Admin Enterprise Edition, and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package.

The package contains new translation messages (in English and French). You should add them to your i18nProvider:

import { Admin } from 'react-admin';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';

import {
    raFormLayoutLanguageEnglish,
    raFormLayoutLanguageFrench,
} from '@react-admin/ra-form-layout';

const messages = {
    en: { ...englishMessages, ...raFormLayoutLanguageEnglish },
    fr: { ...frenchMessages, ...raFormLayoutLanguageFrench },
};

const i18nProvider = polyglotI18nProvider(locale => messages[locale], 'en');

const App = () => <Admin i18nProvider={is18nProvider}>{/* ... */}</Admin>;
import { Admin } from "react-admin";
import polyglotI18nProvider from "ra-i18n-polyglot";
import englishMessages from "ra-language-english";
import frenchMessages from "ra-language-french";

import { raFormLayoutLanguageEnglish, raFormLayoutLanguageFrench } from "@react-admin/ra-form-layout";

const messages = {
    en: { ...englishMessages, ...raFormLayoutLanguageEnglish },
    fr: { ...frenchMessages, ...raFormLayoutLanguageFrench },
};

const i18nProvider = polyglotI18nProvider((locale) => messages[locale], "en");

const App = () => <Admin i18nProvider={is18nProvider}>{/* ... */}</Admin>;

<AccordionForm>

Alternative to <SimpleForm>, to be used as child of <Create> or <Edit>. Expects <AccordionForm.Panel> elements as children.

Test it live in the e-commerce demo.

By default, each child accordion element handles its expanded state independently.

import {
    Edit,
    TextField,
    TextInput,
    DateInput,
    SelectInput,
    ArrayInput,
    SimpleFormIterator,
    BooleanInput,
} from 'react-admin';
import { AccordionForm, AccordionForm.Panel } from '@react-admin/ra-form-layout';

// don't forget the component="div" prop on the main component to disable the main Card
const CustomerEdit = () => (
    <Edit component="div">
        <AccordionForm autoClose>
            <AccordionForm.Panel label="Identity">
                <TextField source="id" />
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput source="dob" label="born" validate={required()} />
                <SelectInput source="sex" choices={sexChoices} />
            </AccordionForm.Panel>
            <AccordionForm.Panel label="Occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </AccordionForm.Panel>
            <AccordionForm.Panel label="Preferences">
                <SelectInput
                    source="language"
                    choices={languageChoices}
                    defaultValue="en"
                />
                <BooleanInput source="dark_theme" />
                <BooleanInput source="accepts_emails_from_partners" />
            </AccordionForm.Panel>
        </AccordionForm>
    </Edit>
);
import {
    Edit,
    TextField,
    TextInput,
    DateInput,
    SelectInput,
    ArrayInput,
    SimpleFormIterator,
    BooleanInput,
} from "react-admin";
import { AccordionForm } from "@react-admin/ra-form-layout";

// don't forget the component="div" prop on the main component to disable the main Card
const CustomerEdit = () => (
    <Edit component="div">
        <AccordionForm autoClose>
            <AccordionForm.Panel label="Identity">
                <TextField source="id" />
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput source="dob" label="born" validate={required()} />
                <SelectInput source="sex" choices={sexChoices} />
            </AccordionForm.Panel>
            <AccordionForm.Panel label="Occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </AccordionForm.Panel>
            <AccordionForm.Panel label="Preferences">
                <SelectInput source="language" choices={languageChoices} defaultValue="en" />
                <BooleanInput source="dark_theme" />
                <BooleanInput source="accepts_emails_from_partners" />
            </AccordionForm.Panel>
        </AccordionForm>
    </Edit>
);

Props

This component accepts the following props in addition to the <Form> props:

Prop Required Type Default Description
authorizationError Optional ReactNode null The content to display when authorization checks fail
autoClose Optional boolean false Enable auto-closing currently opened panel when another is opened
enableAccessControl Optional boolean false Enable checking authorization rights for each panel and input
loading Optional ReactNode The content to display when checking authorizations
toolbar Optional ReactNode The toolbar element
sx Optional object Custom styles

authorizationError

Content displayed when enableAccessControl is set to true and an error occurs while checking for users permissions. Defaults to null:

import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { AccordionForm } from '@react-admin/ra-form-layout';
import { Alert } from '@mui/material';

const CustomerEdit = () => (
    <Edit>
        <AccordionForm
            enableAccessControl
            authorizationError={
                <Alert
                    severity="error"
                    sx={{ px: 2.5, py: 1, mt: 1, width: '100%' }}
                >
                    An error occurred while loading your permissions
                </Alert>
            }
        >
            <AccordionForm.Panel id="identity" defaultExpanded>
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </AccordionForm.Panel>
            <AccordionForm.Panel id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </AccordionForm.Panel>
        </AccordionForm>
    </Edit>
);
import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from "react-admin";
import { AccordionForm } from "@react-admin/ra-form-layout";
import { Alert } from "@mui/material";

const CustomerEdit = () => (
    <Edit>
        <AccordionForm
            enableAccessControl
            authorizationError={
                <Alert severity="error" sx={{ px: 2.5, py: 1, mt: 1, width: "100%" }}>
                    An error occurred while loading your permissions
                </Alert>
            }
        >
            <AccordionForm.Panel id="identity" defaultExpanded>
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </AccordionForm.Panel>
            <AccordionForm.Panel id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </AccordionForm.Panel>
        </AccordionForm>
    </Edit>
);

autoClose

When setting autoClose in the <AccordionForm>, only one accordion remains open at a time. The first accordion is open by default, and when a user opens another one, the current open accordion closes.

import { Edit, TextField, TextInput, DateInput, SelectInput, ArrayInput, SimpleFormIterator, BooleanInput } from 'react-admin';
import { AccordionForm, AccordionForm.Panel } from '@react-admin/ra-form-layout';

// don't forget the component="div" prop on the main component to disable the main Card
const CustomerEdit = (props: EditProps) => (
    <Edit {...props} component="div">
-       <AccordionForm>
+       <AccordionForm autoClose>
            <AccordionForm.Panel label="Identity" defaultExpanded>
                <TextField source="id" />
                ...

enableAccessControl

When set to true, React-admin will call the authProvider.canAccess method for each panel with the following parameters:

  • action: write
  • resource: RESOURCE_NAME.panel.PANEL_ID_OR_LABEL. For instance: customers.panel.identity
  • record: The current record

For each panel, react-admin will also call the authProvider.canAccess method for each input with the following parameters:

  • action: write
  • resource: RESOURCE_NAME.INPUT_SOURCE. For instance: customers.first_name
  • record: The current record

Tip: <AccordionForm.Panel> direct children that don't have a source will always be displayed.

import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { AccordionForm } from '@react-admin/ra-form-layout';

const CustomerEdit = () => (
    <Edit>
        <AccordionForm enableAccessControl>
            <AccordionForm.Panel id="identity" defaultExpanded>
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </AccordionForm.Panel>
            <AccordionForm.Panel id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </AccordionForm.Panel>
        </AccordionForm>
    </Edit>
);
import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from "react-admin";
import { AccordionForm } from "@react-admin/ra-form-layout";

const CustomerEdit = () => (
    <Edit>
        <AccordionForm enableAccessControl>
            <AccordionForm.Panel id="identity" defaultExpanded>
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </AccordionForm.Panel>
            <AccordionForm.Panel id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </AccordionForm.Panel>
        </AccordionForm>
    </Edit>
);

Tip: If you only want access control for the panels but not for the inputs, set the enableAccessControl prop to false on the <AccordionForm.Panel>.

loading

Content displayed when enableAccessControl is set to true while checking for users permissions. Defaults to Loading from react-admin:

import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { AccordionForm } from '@react-admin/ra-form-layout';
import { Typography } from '@mui/material';

const CustomerEdit = () => (
    <Edit>
        <AccordionForm
            enableAccessControl
            loading={
                <Typography>
                    Loading your permissions...
                </Typography>
            }
        >
            <AccordionForm.Panel id="identity" defaultExpanded>
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </AccordionForm.Panel>
            <AccordionForm.Panel id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </AccordionForm.Panel>
        </AccordionForm>
    </Edit>
);
import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from "react-admin";
import { AccordionForm } from "@react-admin/ra-form-layout";
import { Typography } from "@mui/material";

const CustomerEdit = () => (
    <Edit>
        <AccordionForm enableAccessControl loading={<Typography>Loading your permissions...</Typography>}>
            <AccordionForm.Panel id="identity" defaultExpanded>
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </AccordionForm.Panel>
            <AccordionForm.Panel id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </AccordionForm.Panel>
        </AccordionForm>
    </Edit>
);

toolbar

You can customize the form Toolbar by passing a custom element in the toolbar prop. The form expects the same type of element as <SimpleForm>, see the <SimpleForm toolbar> prop documentation in the react-admin docs.

import { Edit, SaveButton, Toolbar as RaToolbar } from 'react-admin';
import { AccordionForm } from '@react-admin/ra-form-layout';

const CustomerCustomToolbar = props => (
    <RaToolbar {...props}>
        <SaveButton label="Save and return" type="button" variant="outlined" />
    </RaToolbar>
);

const CustomerEditWithToolbar = () => (
    <Edit component="div">
        <AccordionForm toolbar={<CustomerCustomToolbar />}>
            <AccordionForm.Panel label="Identity">{/* ... */}</AccordionForm.Panel>
            <AccordionForm.Panel label="Occupations">{/* ... */}</AccordionForm.Panel>
            <AccordionForm.Panel label="Preferences">{/* ... */}</AccordionForm.Panel>
        </AccordionForm>
    </Edit>
);
import { Edit, SaveButton, Toolbar as RaToolbar } from "react-admin";
import { AccordionForm } from "@react-admin/ra-form-layout";

const CustomerCustomToolbar = (props) => (
    <RaToolbar {...props}>
        <SaveButton label="Save and return" type="button" variant="outlined" />
    </RaToolbar>
);

const CustomerEditWithToolbar = () => (
    <Edit component="div">
        <AccordionForm toolbar={<CustomerCustomToolbar />}>
            <AccordionForm.Panel label="Identity">{/* ... */}</AccordionForm.Panel>
            <AccordionForm.Panel label="Occupations">{/* ... */}</AccordionForm.Panel>
            <AccordionForm.Panel label="Preferences">{/* ... */}</AccordionForm.Panel>
        </AccordionForm>
    </Edit>
);

sx: CSS API

The <AccordionForm> component accepts the usual className prop. You can also override the styles of the inner components thanks to the sx property. This property accepts the following subclasses:

Rule name Description
&.MuiBox-root Applied to the root component
&.MuiAccordion-root Applied to all the Accordions
&.Mui-expanded Applied to the expanded Accordions
&.MuiAccordionSummary-root Applied to the Accordion's title
&.MuiCollapse-root Applied to the Accordion's content

<AccordionForm.Panel>

The children of <AccordionForm> must be <AccordionForm.Panel> elements.

This component renders a MUI <Accordion> component. In the <AccordionDetails>, renders each child inside a <FormInput> (the same layout as in <SimpleForm>). |

import {
    Edit,
    TextField,
    TextInput,
    DateInput,
    SelectInput,
    ArrayInput,
    SimpleFormIterator,
    BooleanInput,
} from 'react-admin';

import { AccordionForm } from '@react-admin/ra-form-layout';

const CustomerEdit = () => (
    <Edit component="div">
        <AccordionForm>
            <AccordionForm.Panel label="Identity" defaultExpanded>
                <TextField source="id" />
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput source="dob" label="born" validate={required()} />
                <SelectInput source="sex" choices={sexChoices} />
            </AccordionForm.Panel>
        </AccordionForm>
    </Edit>
);
import { Edit, TextField, TextInput, DateInput, SelectInput } from "react-admin";

import { AccordionForm } from "@react-admin/ra-form-layout";

const CustomerEdit = () => (
    <Edit component="div">
        <AccordionForm>
            <AccordionForm.Panel label="Identity" defaultExpanded>
                <TextField source="id" />
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput source="dob" label="born" validate={required()} />
                <SelectInput source="sex" choices={sexChoices} />
            </AccordionForm.Panel>
        </AccordionForm>
    </Edit>
);

Warning: To use an <AccordionForm.Panel> with the autoClose prop and a React node element as a label, you must specify an id.

Props

Prop Required Type Default Description
children Required ReactNode - A list of <Input> elements
defaultExpanded Optional boolean false Set to true to have the accordion expanded by default (except if autoClose = true on the parent)
disabled Optional boolean false If true, the accordion will be displayed in a disabled state.
count Optional ReactNode - A number to be displayed next to the summary, to quantify it
enableAccessControl Optional boolean false Enable checking authorization rights for each panel and input
id Optional string - An id for this Accordion to be used in the useFormGroup hook and for CSS classes.
label Required string or ReactNode - The main label used as the accordion summary. Appears in red when the accordion has errors
secondary Optional string or ReactNode - The secondary label used as the accordion summary
square Optional boolean false If true, rounded corners are disabled.
sx Optional Object - An object containing the MUI style overrides to apply to the root component.

<AccordionSection>

Renders children (Inputs) inside a MUI <Accordion> element without a Card style. To be used as child of a <SimpleForm> or a <TabbedForm> element.

Prefer <AccordionSection> to <AccordionForm> to always display a list of important inputs, then offer accordions for secondary inputs.

Props

Prop Required Type Default Description
authorizationError Optional Component - The component to use when an error occurs while checking permissions.
Accordion Optional Component - The component to use as the accordion.
AccordionDetails Optional Component - The component to use as the accordion details.
AccordionSummary Optional Component - The component to use as the accordion summary.
count Optional ReactNode - A number to be displayed next to the summary, to quantify it
enableAccessControl Optional boolean - Enable access control to the section and its inputs
label Required string or ReactNode - The main label used as the accordion summary.
loading Optional Component - The component to use while checking permissions.
id Optional string - An id for this Accordion to be used for CSS classes.
children Required ReactNode - A list of <Input> elements
fullWidth Optional boolean - Set to false to disable the Accordion taking the entire form width.
className Optional string - A class name to style the underlying <Accordion>
secondary Optional string or ReactNode - The secondary label used as the accordion summary
defaultExpanded Optional boolean false Set to true to have the accordion expanded by default
disabled Optional boolean false If true, the accordion will be displayed in a disabled state.
square Optional boolean false If true, rounded corners are disabled.
import {
    Edit,
    TextField,
    TextInput,
    DateInput,
    SelectInput,
    ArrayInput,
    SimpleForm,
    SimpleFormIterator,
    BooleanInput,
} from 'react-admin';

import { AccordionForm, AccordionForm.Panel } from '@react-admin/ra-form-layout';

const CustomerEdit = () => (
    <Edit component="div">
        <SimpleForm>
            <TextField source="id" />
            <TextInput source="first_name" validate={required()} />
            <TextInput source="last_name" validate={required()} />
            <DateInput source="dob" label="born" validate={required()} />
            <SelectInput source="sex" choices={sexChoices} />
            <AccordionSection label="Occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </AccordionSection>
            <AccordionSection label="Preferences">
                <SelectInput
                    source="language"
                    choices={languageChoices}
                    defaultValue="en"
                />
                <BooleanInput source="dark_theme" />
                <BooleanInput source="accepts_emails_from_partners" />
            </AccordionSection>
        </SimpleForm>
    </Edit>
);
import {
    Edit,
    TextField,
    TextInput,
    DateInput,
    SelectInput,
    ArrayInput,
    SimpleForm,
    SimpleFormIterator,
    BooleanInput,
} from "react-admin";

const CustomerEdit = () => (
    <Edit component="div">
        <SimpleForm>
            <TextField source="id" />
            <TextInput source="first_name" validate={required()} />
            <TextInput source="last_name" validate={required()} />
            <DateInput source="dob" label="born" validate={required()} />
            <SelectInput source="sex" choices={sexChoices} />
            <AccordionSection label="Occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </AccordionSection>
            <AccordionSection label="Preferences">
                <SelectInput source="language" choices={languageChoices} defaultValue="en" />
                <BooleanInput source="dark_theme" />
                <BooleanInput source="accepts_emails_from_partners" />
            </AccordionSection>
        </SimpleForm>
    </Edit>
);

accessDenied

Content displayed when enableAccessControl is set to true and users don't have access to the section. Defaults to null:

import { ReactNode } from 'react';
import { ArrayInput, BooleanInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { AccordionSection } from '@react-admin/ra-form-layout';
import { Alert } from '@mui/material';

const AccessDenied = ({ children }: { children: ReactNode }) => (
    <Alert
        severity="info"
        sx={{ px: 2.5, py: 1, mt: 1, width: '100%' }}
        action={
            <Button color="inherit" size="small">
                Upgrade to Premium
            </Button>
        }
    >
        {children}
    </Alert>
)

const CustomerEdit = () => (
    <Edit component="div">
        <SimpleForm>
            <TextInput source="first_name" validate={required()} />
            <TextInput source="last_name" validate={required()} />
            <AccordionSection
                label="Preferences"
                enableAccessControl
                accessDenied={<AccessDenied>You don&apos;t have access to the preferences section</AccessDenied>}
            >
                <SelectInput
                    source="language"
                    choices={languageChoices}
                    defaultValue="en"
                />
                <BooleanInput source="dark_theme" />
                <BooleanInput source="accepts_emails_from_partners" />
            </AccordionSection>
        </SimpleForm>
    </Edit>
);
import { BooleanInput, Edit, TextInput } from "react-admin";
import { AccordionSection } from "@react-admin/ra-form-layout";
import { Alert } from "@mui/material";

const AccessDenied = ({ children }) => (
    <Alert
        severity="info"
        sx={{ px: 2.5, py: 1, mt: 1, width: "100%" }}
        action={
            <Button color="inherit" size="small">
                Upgrade to Premium
            </Button>
        }
    >
        {children}
    </Alert>
);

const CustomerEdit = () => (
    <Edit component="div">
        <SimpleForm>
            <TextInput source="first_name" validate={required()} />
            <TextInput source="last_name" validate={required()} />
            <AccordionSection
                label="Preferences"
                enableAccessControl
                accessDenied={<AccessDenied>You don&apos;t have access to the preferences section</AccessDenied>}
            >
                <SelectInput source="language" choices={languageChoices} defaultValue="en" />
                <BooleanInput source="dark_theme" />
                <BooleanInput source="accepts_emails_from_partners" />
            </AccordionSection>
        </SimpleForm>
    </Edit>
);

authorizationError

Content displayed when enableAccessControl is set to true and an error occurs while checking for users permissions. Defaults to null:

import { ArrayInput, BooleanInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { AccordionSection } from '@react-admin/ra-form-layout';
import { Alert } from '@mui/material';

const AuthorizationError = () => (
    <Alert
        severity="error"
        sx={{ px: 2.5, py: 1, mt: 1, width: '100%' }}
    >
        An error occurred while loading your permissions
    </Alert>
);

const CustomerEdit = () => (
    <Edit component="div">
        <SimpleForm>
            <TextInput source="first_name" validate={required()} />
            <TextInput source="last_name" validate={required()} />
            <AccordionSection label="Preferences" enableAccessControl authorizationError={<AuthorizationError />}>
                <SelectInput
                    source="language"
                    choices={languageChoices}
                    defaultValue="en"
                />
                <BooleanInput source="dark_theme" />
                <BooleanInput source="accepts_emails_from_partners" />
            </AccordionSection>
        </SimpleForm>
    </Edit>
);
import { BooleanInput, Edit, TextInput } from "react-admin";
import { AccordionSection } from "@react-admin/ra-form-layout";
import { Alert } from "@mui/material";

const AuthorizationError = () => (
    <Alert severity="error" sx={{ px: 2.5, py: 1, mt: 1, width: "100%" }}>
        An error occurred while loading your permissions
    </Alert>
);

const CustomerEdit = () => (
    <Edit component="div">
        <SimpleForm>
            <TextInput source="first_name" validate={required()} />
            <TextInput source="last_name" validate={required()} />
            <AccordionSection label="Preferences" enableAccessControl authorizationError={<AuthorizationError />}>
                <SelectInput source="language" choices={languageChoices} defaultValue="en" />
                <BooleanInput source="dark_theme" />
                <BooleanInput source="accepts_emails_from_partners" />
            </AccordionSection>
        </SimpleForm>
    </Edit>
);

enableAccessControl

When set to true, React-admin will call the authProvider.canAccess method the following parameters:

  • action: write
  • resource: RESOURCE_NAME.section.PANEL_ID_OR_LABEL. For instance: customers.section.identity
  • record: The current record

React-admin will also call the authProvider.canAccess method for each input with the following parameters:

  • action: write
  • resource: RESOURCE_NAME.INPUT_SOURCE. For instance: customers.first_name
  • record: The current record
import { ArrayInput, BooleanInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { AccordionSection } from '@react-admin/ra-form-layout';

const CustomerEdit = () => (
    <Edit component="div">
        <SimpleForm>
            <TextField source="id" />
            <TextInput source="first_name" validate={required()} />
            <TextInput source="last_name" validate={required()} />
            <DateInput source="dob" label="born" validate={required()} />
            <SelectInput source="sex" choices={sexChoices} />
            <AccordionSection label="Preferences" enableAccessControl>
                <SelectInput
                    source="language"
                    choices={languageChoices}
                    defaultValue="en"
                />
                <BooleanInput source="dark_theme" />
                <BooleanInput source="accepts_emails_from_partners" />
            </AccordionSection>
        </SimpleForm>
    </Edit>
);
import { BooleanInput, Edit, DateInput, TextInput } from "react-admin";
import { AccordionSection } from "@react-admin/ra-form-layout";

const CustomerEdit = () => (
    <Edit component="div">
        <SimpleForm>
            <TextField source="id" />
            <TextInput source="first_name" validate={required()} />
            <TextInput source="last_name" validate={required()} />
            <DateInput source="dob" label="born" validate={required()} />
            <SelectInput source="sex" choices={sexChoices} />
            <AccordionSection label="Preferences" enableAccessControl>
                <SelectInput source="language" choices={languageChoices} defaultValue="en" />
                <BooleanInput source="dark_theme" />
                <BooleanInput source="accepts_emails_from_partners" />
            </AccordionSection>
        </SimpleForm>
    </Edit>
);

Tip: <AccordionSection> direct children that don't have a source will always be displayed.

loading

Content displayed when enableAccessControl is set to true while checking for users permissions. Defaults to Loading from react-admin:

import { ArrayInput, BooleanInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { AccordionSection } from '@react-admin/ra-form-layout';
import { Typography } from '@mui/material';

const AuthorizationLoading = () => (
    <Typography>
        Loading your permissions...
    </Typography>
);

const CustomerEdit = () => (
    <Edit component="div">
        <SimpleForm>
            <TextField source="id" />
            <TextInput source="first_name" validate={required()} />
            <TextInput source="last_name" validate={required()} />
            <DateInput source="dob" label="born" validate={required()} />
            <SelectInput source="sex" choices={sexChoices} />
            <AccordionSection label="Preferences" enableAccessControl loading={<AuthorizationLoading />}>
                <SelectInput
                    source="language"
                    choices={languageChoices}
                    defaultValue="en"
                />
                <BooleanInput source="dark_theme" />
                <BooleanInput source="accepts_emails_from_partners" />
            </AccordionSection>
        </SimpleForm>
    </Edit>
);
import { BooleanInput, Edit, DateInput, TextInput } from "react-admin";
import { AccordionSection } from "@react-admin/ra-form-layout";
import { Typography } from "@mui/material";

const AuthorizationLoading = () => <Typography>Loading your permissions...</Typography>;

const CustomerEdit = () => (
    <Edit component="div">
        <SimpleForm>
            <TextField source="id" />
            <TextInput source="first_name" validate={required()} />
            <TextInput source="last_name" validate={required()} />
            <DateInput source="dob" label="born" validate={required()} />
            <SelectInput source="sex" choices={sexChoices} />
            <AccordionSection label="Preferences" enableAccessControl loading={<AuthorizationLoading />}>
                <SelectInput source="language" choices={languageChoices} defaultValue="en" />
                <BooleanInput source="dark_theme" />
                <BooleanInput source="accepts_emails_from_partners" />
            </AccordionSection>
        </SimpleForm>
    </Edit>
);

<WizardForm>

Alternative to <SimpleForm> that splits a form into a step-by-step interface, to facilitate the entry in long forms.

Test it live in the e-commerce demo.

Use <WizardForm> as the child of <Create>. It expects <WizardFormStep> elements as children.

import React from 'react';
import { Create, TextInput, required } from 'react-admin';
import { WizardForm } from '@react-admin/ra-form-layout';

const PostCreate = () => (
    <Create>
        <WizardForm>
            <WizardForm.Step label="First step">
                <TextInput source="title" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step label="Second step">
                <TextInput source="description" />
            </WizardForm.Step>
            <WizardForm.Step label="Third step">
                <TextInput source="fullDescription" validate={required()} />
            </WizardForm.Step>
        </WizardForm>
    </Create>
);
import React from "react";
import { Create, TextInput, required } from "react-admin";
import { WizardForm } from "@react-admin/ra-form-layout";

const PostCreate = () => (
    <Create>
        <WizardForm>
            <WizardForm.Step label="First step">
                <TextInput source="title" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step label="Second step">
                <TextInput source="description" />
            </WizardForm.Step>
            <WizardForm.Step label="Third step">
                <TextInput source="fullDescription" validate={required()} />
            </WizardForm.Step>
        </WizardForm>
    </Create>
);

Note: You can also use the <WizardForm> as child of <Edit> but it's considered as a bad practice to provide a wizard form for existing resources.

Tip: The label prop of the <WizardForm.Step> component accepts a translation key:

import React from 'react';
import { Create, TextInput, required } from 'react-admin';
import { WizardForm } from '@react-admin/ra-form-layout';

const PostCreate = () => (
    <Create>
        <WizardForm>
            <WizardForm.Step label="myapp.posts.steps.general">
                <TextInput source="title" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step label="myapp.posts.steps.description">
                <TextInput source="description" />
            </WizardForm.Step>
            <WizardForm.Step label="myapp.posts.steps.misc">
                <TextInput source="fullDescription" validate={required()} />
            </WizardForm.Step>
        </WizardForm>
    </Create>
);
import React from "react";
import { Create, TextInput, required } from "react-admin";
import { WizardForm } from "@react-admin/ra-form-layout";

const PostCreate = () => (
    <Create>
        <WizardForm>
            <WizardForm.Step label="myapp.posts.steps.general">
                <TextInput source="title" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step label="myapp.posts.steps.description">
                <TextInput source="description" />
            </WizardForm.Step>
            <WizardForm.Step label="myapp.posts.steps.misc">
                <TextInput source="fullDescription" validate={required()} />
            </WizardForm.Step>
        </WizardForm>
    </Create>
);

Props

This component accepts the following props in addition to the <Form> props:

Prop Required Type Default Description
authorizationError Optional ReactNode null The content to display when authorization checks fail
enableAccessControl Optional boolean false Enable checking authorization rights for each panel and input
loading Optional ReactNode The content to display when checking authorizations
progress Optional ReactNode The progress element
toolbar Optional ReactNode The toolbar element

authorizationError

Used when enableAccessControl is set to true and an error occurs while checking for users permissions. Defaults to null:

import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { WizardForm } from '@react-admin/ra-form-layout';
import { Alert } from '@mui/material';

const CustomerEdit = () => (
    <Edit>
        <WizardForm
            enableAccessControl
            authorizationError={
                <Alert
                    severity="error"
                    sx={{ px: 2.5, py: 1, mt: 1, width: '100%' }}
                >
                    An error occurred while loading your permissions
                </Alert>
            }
        >
            <WizardForm.Step id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </WizardForm.Step>
        </WizardForm>
    </Edit>
);
import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from "react-admin";
import { WizardForm } from "@react-admin/ra-form-layout";
import { Alert } from "@mui/material";

const CustomerEdit = () => (
    <Edit>
        <WizardForm
            enableAccessControl
            authorizationError={
                <Alert severity="error" sx={{ px: 2.5, py: 1, mt: 1, width: "100%" }}>
                    An error occurred while loading your permissions
                </Alert>
            }
        >
            <WizardForm.Step id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </WizardForm.Step>
        </WizardForm>
    </Edit>
);

enableAccessControl

When set to true, React-admin will call the authProvider.canAccess method for each panel with the following parameters:

  • action: write
  • resource: RESOURCE_NAME.section.PANEL_ID_OR_LABEL. For instance: customers.section.identity
  • record: The current record

For each panel, react-admin will also call the authProvider.canAccess method for each input with the following parameters:

  • action: write
  • resource: RESOURCE_NAME.INPUT_SOURCE. For instance: customers.first_name
  • record: The current record

Tip: <WizardForm.Step> direct children that don't have a source will always be displayed.

import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { WizardForm } from '@react-admin/ra-form-layout';

const CustomerEdit = () => (
    <Edit>
        <WizardForm enableAccessControl>
            <WizardForm.Step id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </WizardForm.Step>
        </WizardForm>
    </Edit>
);
import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from "react-admin";
import { WizardForm } from "@react-admin/ra-form-layout";

const CustomerEdit = () => (
    <Edit>
        <WizardForm enableAccessControl>
            <WizardForm.Step id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </WizardForm.Step>
        </WizardForm>
    </Edit>
);

loading

Used when enableAccessControl is set to true while checking for users permissions. Defaults to Loading from react-admin:

import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { WizardForm } from '@react-admin/ra-form-layout';
import { Typography } from '@mui/material';

const CustomerEdit = () => (
    <Edit>
        <WizardForm
            enableAccessControl
            loading={
                <Typography>
                    Loading your permissions...
                </Typography>
            }
        >
            <WizardForm.Step id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </WizardForm.Step>
        </WizardForm>
    </Edit>
);
import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from "react-admin";
import { WizardForm } from "@react-admin/ra-form-layout";
import { Typography } from "@mui/material";

const CustomerEdit = () => (
    <Edit>
        <WizardForm enableAccessControl loading={<Typography>Loading your permissions...</Typography>}>
            <WizardForm.Step id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </WizardForm.Step>
        </WizardForm>
    </Edit>
);

progress

You can also customize the progress stepper by passing a custom component in the progress prop.

import React from 'react';
import { Create, TextInput, required } from 'react-admin';
import {
    WizardForm,
    WizardFormProgressProps,
    useWizardFormContext
} from '@react-admin/ra-form-layout';

const MyProgress = (props: WizardFormProgressProps) => {
    const { currentStep, steps } = useWizardFormContext(props);
    return (
        <ul>
            {steps.map((step, index) => {
                const label = React.cloneElement(step, { intent: 'label' });
                return (
                    <li key={`step_${index}`}>
                        <span
                            style={{
                                textDecoration:
                                    currentStep === index
                                        ? 'underline'
                                        : undefined,
                            }}
                        >
                            {label}
                        </span>
                    </li>
                );
            })}
        </ul>
    );
};

const PostCreate = () => (
    <Create>
        <WizardForm progress={<MyProgress />}>
            <WizardForm.Step label="First step">
                <TextInput source="title" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step label="Second step">
                <TextInput source="description" />
            </WizardForm.Step>
            <WizardForm.Step label="Third step">
                <TextInput source="fullDescription" validate={required()} />
            </WizardForm.Step>
        </WizardForm>
    </Create>
);
import React from "react";
import { Create, TextInput, required } from "react-admin";
import { WizardForm, useWizardFormContext } from "@react-admin/ra-form-layout";

const MyProgress = (props) => {
    const { currentStep, steps } = useWizardFormContext(props);
    return (
        <ul>
            {steps.map((step, index) => {
                const label = React.cloneElement(step, { intent: "label" });
                return (
                    <li key={`step_${index}`}>
                        <span
                            style={{
                                textDecoration: currentStep === index ? "underline" : undefined,
                            }}
                        >
                            {label}
                        </span>
                    </li>
                );
            })}
        </ul>
    );
};

const PostCreate = () => (
    <Create>
        <WizardForm progress={<MyProgress />}>
            <WizardForm.Step label="First step">
                <TextInput source="title" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step label="Second step">
                <TextInput source="description" />
            </WizardForm.Step>
            <WizardForm.Step label="Third step">
                <TextInput source="fullDescription" validate={required()} />
            </WizardForm.Step>
        </WizardForm>
    </Create>
);

Any additional props will be passed to the <Progress> component.

You can also hide the progress stepper completely by setting progress to false.

import React from 'react';
import { Create, TextInput, required } from 'react-admin';
import { WizardForm } from '@react-admin/ra-form-layout';

const PostCreate = () => (
    <Create>
        <WizardForm progress={false}>
            <WizardForm.Step label="First step">
                <TextInput source="title" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step label="Second step">
                <TextInput source="description" />
            </WizardForm.Step>
            <WizardForm.Step label="Third step">
                <TextInput source="fullDescription" validate={required()} />
            </WizardForm.Step>
        </WizardForm>
    </Create>
);
import React from "react";
import { Create, TextInput, required } from "react-admin";
import { WizardForm } from "@react-admin/ra-form-layout";

const PostCreate = () => (
    <Create>
        <WizardForm progress={false}>
            <WizardForm.Step label="First step">
                <TextInput source="title" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step label="Second step">
                <TextInput source="description" />
            </WizardForm.Step>
            <WizardForm.Step label="Third step">
                <TextInput source="fullDescription" validate={required()} />
            </WizardForm.Step>
        </WizardForm>
    </Create>
);

toolbar

You can customize the form toolbar by passing a custom component in the toolbar prop.

import { Button } from '@mui/material';
import React from 'react';
import { Create, required, TextInput, useSaveContext } from 'react-admin';
import { useFormState } from 'react-hook-form';
import { useWizardFormContext, WizardForm } from '@react-admin/ra-form-layout';

const MyToolbar = () => {
    const { hasNextStep, hasPreviousStep, goToNextStep, goToPreviousStep } =
        useWizardFormContext();
    const { save } = useSaveContext();
    const { isValidating } = useFormState();

    return (
        <ul>
            {hasPreviousStep ? (
                <li>
                    <Button onClick={() => goToPreviousStep()}>PREVIOUS</Button>
                </li>
            ) : null}
            {hasNextStep ? (
                <li>
                    <Button
                        disabled={isValidating}
                        onClick={() => goToNextStep()}
                    >
                        NEXT
                    </Button>
                </li>
            ) : (
                <li>
                    <Button disabled={isValidating} onClick={save}>
                        SAVE
                    </Button>
                </li>
            )}
        </ul>
    );
};

const PostCreate = () => (
    <Create>
        <WizardForm toolbar={<MyToolbar />}>
            <WizardForm.Step label="First step">
                <TextInput source="title" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step label="Second step">
                <TextInput source="description" />
            </WizardForm.Step>
            <WizardForm.Step label="Third step">
                <TextInput source="fullDescription" validate={required()} />
            </WizardForm.Step>
        </WizardForm>
    </Create>
);
import { Button } from "@mui/material";
import React from "react";
import { Create, required, TextInput, useSaveContext } from "react-admin";
import { useFormState } from "react-hook-form";
import { useWizardFormContext, WizardForm } from "@react-admin/ra-form-layout";

const MyToolbar = () => {
    const { hasNextStep, hasPreviousStep, goToNextStep, goToPreviousStep } = useWizardFormContext();
    const { save } = useSaveContext();
    const { isValidating } = useFormState();

    return (
        <ul>
            {hasPreviousStep ? (
                <li>
                    <Button onClick={() => goToPreviousStep()}>PREVIOUS</Button>
                </li>
            ) : null}
            {hasNextStep ? (
                <li>
                    <Button disabled={isValidating} onClick={() => goToNextStep()}>
                        NEXT
                    </Button>
                </li>
            ) : (
                <li>
                    <Button disabled={isValidating} onClick={save}>
                        SAVE
                    </Button>
                </li>
            )}
        </ul>
    );
};

const PostCreate = () => (
    <Create>
        <WizardForm toolbar={<MyToolbar />}>
            <WizardForm.Step label="First step">
                <TextInput source="title" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step label="Second step">
                <TextInput source="description" />
            </WizardForm.Step>
            <WizardForm.Step label="Third step">
                <TextInput source="fullDescription" validate={required()} />
            </WizardForm.Step>
        </WizardForm>
    </Create>
);

Adding a Summary Final Step

In order to add a final step with a summary of the form values before submit, you can leverage react-hook-form useWatch hook:

const FinalStepContent = () => {
    const values = useWatch({
        name: ['title', 'description', 'fullDescription'],
    });

    return values?.length > 0 ? (
        <>
            <Typography>title: {values[0]}</Typography>
            <Typography>description: {values[1]}</Typography>
            <Typography>fullDescription: {values[2]}</Typography>
        </>
    ) : null;
};

const PostCreate = () => (
    <Create>
        <WizardForm>
            <WizardForm.Step label="First step">
                <TextInput source="title" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step label="Second step">
                <TextInput source="description" />
            </WizardForm.Step>
            <WizardForm.Step label="Third step">
                <TextInput source="fullDescription" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step label="">
                <FinalStepContent />
            </WizardForm.Step>
        </WizardForm>
    </Create>
);
const FinalStepContent = () => {
    const values = useWatch({
        name: ["title", "description", "fullDescription"],
    });

    return values?.length > 0 ? (
        <>
            <Typography>title: {values[0]}</Typography>
            <Typography>description: {values[1]}</Typography>
            <Typography>fullDescription: {values[2]}</Typography>
        </>
    ) : null;
};

const PostCreate = () => (
    <Create>
        <WizardForm>
            <WizardForm.Step label="First step">
                <TextInput source="title" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step label="Second step">
                <TextInput source="description" />
            </WizardForm.Step>
            <WizardForm.Step label="Third step">
                <TextInput source="fullDescription" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step label="">
                <FinalStepContent />
            </WizardForm.Step>
        </WizardForm>
    </Create>
);

<WizardForm.Step>

The children of <WizardForm> must be <WizardForm.Step> elements.

It accepts the following props:

Prop Required Type Default Description
authorizationError Optional ReactNode - The content to display when authorization checks fail
enableAccessControl Optional ReactNode - Enable authorization checks
label Required string - The main label used as the step title. Appears in red when the section has errors
loading Optional ReactNode - The content to display while checking authorizations
children Required ReactNode - A list of <Input> elements
sx Optional object - An object containing the MUI style overrides to apply to the root component
authorizationError

Used when enableAccessControl is set to true and an error occurs while checking for users permissions. Defaults to null:

import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { WizardForm } from '@react-admin/ra-form-layout';
import { Alert } from '@mui/material';

const CustomerEdit = () => (
    <Edit>
        <WizardForm enableAccessControl>
            <WizardForm.Step id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step id="occupations" authorizationError={
                <Alert
                    severity="error"
                    sx={{ px: 2.5, py: 1, mt: 1, width: '100%' }}
                >
                    An error occurred while loading your permissions
                </Alert>
            }>
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </WizardForm.Step>
        </WizardForm>
    </Edit>
);
import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from "react-admin";
import { WizardForm } from "@react-admin/ra-form-layout";
import { Alert } from "@mui/material";

const CustomerEdit = () => (
    <Edit>
        <WizardForm enableAccessControl>
            <WizardForm.Step id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step
                id="occupations"
                authorizationError={
                    <Alert severity="error" sx={{ px: 2.5, py: 1, mt: 1, width: "100%" }}>
                        An error occurred while loading your permissions
                    </Alert>
                }
            >
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </WizardForm.Step>
        </WizardForm>
    </Edit>
);
enableAccessControl

When set to true, react-admin will also call the authProvider.canAccess method for each input with the following parameters:

  • action: write
  • resource: RESOURCE_NAME.INPUT_SOURCE. For instance: customers.first_name
  • record: The current record

Tip: <WizardForm.Step> direct children that don't have a source will always be displayed.

import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { WizardForm } from '@react-admin/ra-form-layout';

const CustomerEdit = () => (
    <Edit>
        <WizardForm>
            <WizardForm.Step id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step id="occupations" enableAccessControl>
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </WizardForm.Step>
        </WizardForm>
    </Edit>
);
import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from "react-admin";
import { WizardForm } from "@react-admin/ra-form-layout";

const CustomerEdit = () => (
    <Edit>
        <WizardForm>
            <WizardForm.Step id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step id="occupations" enableAccessControl>
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </WizardForm.Step>
        </WizardForm>
    </Edit>
);

loading

Used when enableAccessControl is set to true while checking for users permissions. Defaults to Loading from react-admin:

import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { WizardForm } from '@react-admin/ra-form-layout';
import { Typography } from '@mui/material';

const CustomerEdit = () => (
    <Edit>
        <WizardForm enableAccessControl>
            <WizardForm.Step id="identity" loading={
                <Typography>
                    Loading your permissions...
                </Typography>
            }>
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step id="occupations" loading={
                <Typography>
                    Loading your permissions...
                </Typography>
            }>
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </WizardForm.Step>
        </WizardForm>
    </Edit>
);
import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from "react-admin";
import { WizardForm } from "@react-admin/ra-form-layout";
import { Typography } from "@mui/material";

const CustomerEdit = () => (
    <Edit>
        <WizardForm enableAccessControl>
            <WizardForm.Step id="identity" loading={<Typography>Loading your permissions...</Typography>}>
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </WizardForm.Step>
            <WizardForm.Step id="occupations" loading={<Typography>Loading your permissions...</Typography>}>
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </WizardForm.Step>
        </WizardForm>
    </Edit>
);

<LongForm>

Alternative to <SimpleForm>, to be used as child of <Create> or <Edit>. Expects <LongForm.Section> elements as children.

Test it live on the Enterprise Edition Storybook.

This component will come in handy if you need to create a long form, with many input fields divided into several sections. It makes navigation easier, by providing a TOC (Table Of Contents) and by keeping the toolbar fixed at the bottom position.

import {
    ArrayInput,
    BooleanInput,
    DateInput,
    Edit,
    required,
    SelectInput,
    SimpleFormIterator,
    TextField,
    TextInput,
    Labeled,
} from 'react-admin';
import { LongForm } from '@react-admin/ra-form-layout';

const sexChoices = [
    { id: 'male', name: 'Male' },
    { id: 'female', name: 'Female' },
];

const languageChoices = [
    { id: 'en', name: 'English' },
    { id: 'fr', name: 'French' },
];

const CustomerEdit = () => (
    <Edit component="div">
        <LongForm>
            <LongForm.Section label="Identity">
                <Labeled label="id">
                    <TextField source="id" />
                </Labeled>
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput source="dob" label="born" validate={required()} />
                <SelectInput source="sex" choices={sexChoices} />
            </LongForm.Section>
            <LongForm.Section label="Occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </LongForm.Section>
            <LongForm.Section label="Preferences">
                <SelectInput
                    source="language"
                    choices={languageChoices}
                    defaultValue="en"
                />
                <BooleanInput source="dark_theme" />
                <BooleanInput source="accepts_emails_from_partners" />
            </LongForm.Section>
        </LongForm>
    </Edit>
);
import {
    ArrayInput,
    BooleanInput,
    DateInput,
    Edit,
    required,
    SelectInput,
    SimpleFormIterator,
    TextField,
    TextInput,
    Labeled,
} from "react-admin";
import { LongForm } from "@react-admin/ra-form-layout";

const sexChoices = [
    { id: "male", name: "Male" },
    { id: "female", name: "Female" },
];

const languageChoices = [
    { id: "en", name: "English" },
    { id: "fr", name: "French" },
];

const CustomerEdit = () => (
    <Edit component="div">
        <LongForm>
            <LongForm.Section label="Identity">
                <Labeled label="id">
                    <TextField source="id" />
                </Labeled>
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput source="dob" label="born" validate={required()} />
                <SelectInput source="sex" choices={sexChoices} />
            </LongForm.Section>
            <LongForm.Section label="Occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </LongForm.Section>
            <LongForm.Section label="Preferences">
                <SelectInput source="language" choices={languageChoices} defaultValue="en" />
                <BooleanInput source="dark_theme" />
                <BooleanInput source="accepts_emails_from_partners" />
            </LongForm.Section>
        </LongForm>
    </Edit>
);

Props

This component accepts the following props in addition to the <Form> props:

Prop Required Type Default Description
authorizationError Optional ReactNode null The content to display when authorization checks fail
enableAccessControl Optional boolean false Enable checking authorization rights for each section and input
loading Optional ReactNode The content to display when checking authorizations
toolbar Optional ReactNode The toolbar element
sx Optional object Custom styles

authorizationError

Used when enableAccessControl is set to true and an error occurs while checking for users permissions. Defaults to null:

import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { LongForm } from '@react-admin/ra-form-layout';
import { Alert } from '@mui/material';

const CustomerEdit = () => (
    <Edit>
        <LongForm
            enableAccessControl
            authorizationError={
                <Alert
                    severity="error"
                    sx={{ px: 2.5, py: 1, mt: 1, width: '100%' }}
                >
                    An error occurred while loading your permissions
                </Alert>
            }
        >
            <LongForm.Section id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </LongForm.Section>
            <LongForm.Section id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </LongForm.Section>
        </LongForm>
    </Edit>
);
import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from "react-admin";
import { LongForm } from "@react-admin/ra-form-layout";
import { Alert } from "@mui/material";

const CustomerEdit = () => (
    <Edit>
        <LongForm
            enableAccessControl
            authorizationError={
                <Alert severity="error" sx={{ px: 2.5, py: 1, mt: 1, width: "100%" }}>
                    An error occurred while loading your permissions
                </Alert>
            }
        >
            <LongForm.Section id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </LongForm.Section>
            <LongForm.Section id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </LongForm.Section>
        </LongForm>
    </Edit>
);

enableAccessControl

When set to true, React-admin will call the authProvider.canAccess method for each section with the following parameters:

  • action: write
  • resource: RESOURCE_NAME.section.SECTION_ID_OR_LABEL. For instance: customers.section.identity
  • record: The current record

For each section, react-admin will also call the authProvider.canAccess method for each input with the following parameters:

  • action: write
  • resource: RESOURCE_NAME.INPUT_SOURCE. For instance: customers.first_name
  • record: The current record

Tip: <LongForm.Section> direct children that don't have a source will always be displayed.

import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { LongForm } from '@react-admin/ra-form-layout';

const CustomerEdit = () => (
    <Edit>
        <LongForm enableAccessControl>
            <LongForm.Section id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </LongForm.Section>
            <LongForm.Section id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </LongForm.Section>
        </LongForm>
    </Edit>
);
import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from "react-admin";
import { LongForm } from "@react-admin/ra-form-layout";

const CustomerEdit = () => (
    <Edit>
        <LongForm enableAccessControl>
            <LongForm.Section id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </LongForm.Section>
            <LongForm.Section id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </LongForm.Section>
        </LongForm>
    </Edit>
);

loading

Used when enableAccessControl is set to true while checking for users permissions. Defaults to Loading from react-admin:

import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { LongForm } from '@react-admin/ra-form-layout';
import { Typography } from '@mui/material';

const CustomerEdit = () => (
    <Edit>
        <LongForm
            enableAccessControl
            loading={
                <Typography>
                    Loading your permissions...
                </Typography>
            }
        >
            <LongForm.Section id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </LongForm.Section>
            <LongForm.Section id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </LongForm.Section>
        </LongForm>
    </Edit>
);
import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from "react-admin";
import { LongForm } from "@react-admin/ra-form-layout";
import { Typography } from "@mui/material";

const CustomerEdit = () => (
    <Edit>
        <LongForm enableAccessControl loading={<Typography>Loading your permissions...</Typography>}>
            <LongForm.Section id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </LongForm.Section>
            <LongForm.Section id="occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </LongForm.Section>
        </LongForm>
    </Edit>
);

toolbar

You can customize the form Toolbar by passing a custom element in the toolbar prop. The form expects the same type of element as <SimpleForm>, see the <SimpleForm toolbar> prop documentation in the react-admin docs.

import { Edit, SaveButton, Toolbar as RaToolbar } from 'react-admin';
import { LongForm } from '@react-admin/ra-form-layout';

const CustomerCustomToolbar = props => (
    <RaToolbar {...props}>
        <SaveButton label="Save and return" type="button" variant="outlined" />
    </RaToolbar>
);

const CustomerEditWithToolbar = () => (
    <Edit component="div">
        <LongForm toolbar={<CustomerCustomToolbar />}>
            <LongForm.Section label="Identity">...</LongForm.Section>
            <LongForm.Section label="Occupations">...</LongForm.Section>
            <LongForm.Section label="Preferences">...</LongForm.Section>
        </LongForm>
    </Edit>
);
import { Edit, SaveButton, Toolbar as RaToolbar } from "react-admin";
import { LongForm } from "@react-admin/ra-form-layout";

const CustomerCustomToolbar = (props) => (
    <RaToolbar {...props}>
        <SaveButton label="Save and return" type="button" variant="outlined" />
    </RaToolbar>
);

const CustomerEditWithToolbar = () => (
    <Edit component="div">
        <LongForm toolbar={<CustomerCustomToolbar />}>
            <LongForm.Section label="Identity">...</LongForm.Section>
            <LongForm.Section label="Occupations">...</LongForm.Section>
            <LongForm.Section label="Preferences">...</LongForm.Section>
        </LongForm>
    </Edit>
);

sx: CSS API

The <LongForm> component accepts the usual className prop. You can also override the styles of the inner components thanks to the sx property. This property accepts the following subclasses:

Rule name Description
&.RaLongForm-root Applied to the root component
& .RaLongForm-toc Applied to the TOC
& .RaLongForm-main Applied to the main <Card> component
& .RaLongForm-toolbar Applied to the toolbar
& .RaLongForm-error Applied to the <MenuItem> in case the section has validation errors

<LongForm.Section>

The children of <LongForm> must be <LongForm.Section> elements.

This component adds a section title (using a <Typography variant="h4">), then renders each child inside a MUI <Stack>, and finally adds an MUI <Divider> at the bottom of the section.

It accepts the following props:

Prop Required Type Default Description
authorizationError Optional ReactNode - The content to display when authorization checks fail
enableAccessControl Optional ReactNode - Enable authorization checks
label Required string - The main label used as the section title. Appears in red when the section has errors
loading Optional ReactNode - The content to display while checking authorizations
children Required ReactNode - A list of <Input> elements
cardinality Optional number - A number to be displayed next to the label in TOC, to quantify it
sx Optional object - An object containing the MUI style overrides to apply to the root component
authorizationError

Used when enableAccessControl is set to true and an error occurs while checking for users permissions. Defaults to null:

import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { LongForm } from '@react-admin/ra-form-layout';
import { Alert } from '@mui/material';

const CustomerEdit = () => (
    <Edit>
        <LongForm enableAccessControl>
            <LongForm.Section id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </LongForm.Section>
            <LongForm.Section id="occupations">
                <ArrayInput source="occupations" label="" authorizationError={
                <Alert
                    severity="error"
                    sx={{ px: 2.5, py: 1, mt: 1, width: '100%' }}
                >
                    An error occurred while loading your permissions
                </Alert>
            }>
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </LongForm.Section>
        </LongForm>
    </Edit>
);
import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from "react-admin";
import { LongForm } from "@react-admin/ra-form-layout";
import { Alert } from "@mui/material";

const CustomerEdit = () => (
    <Edit>
        <LongForm enableAccessControl>
            <LongForm.Section id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </LongForm.Section>
            <LongForm.Section id="occupations">
                <ArrayInput
                    source="occupations"
                    label=""
                    authorizationError={
                        <Alert severity="error" sx={{ px: 2.5, py: 1, mt: 1, width: "100%" }}>
                            An error occurred while loading your permissions
                        </Alert>
                    }
                >
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </LongForm.Section>
        </LongForm>
    </Edit>
);
enableAccessControl

When set to true, react-admin will also call the authProvider.canAccess method for each input with the following parameters:

  • action: write
  • resource: RESOURCE_NAME.INPUT_SOURCE. For instance: customers.first_name
  • record: The current record

Tip: <LongForm.Section> direct children that don't have a source will always be displayed.

import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { LongForm } from '@react-admin/ra-form-layout';

const CustomerEdit = () => (
    <Edit>
        <LongForm>
            <LongForm.Section id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </LongForm.Section>
            <LongForm.Section id="occupations" enableAccessControl>
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </LongForm.Section>
        </LongForm>
    </Edit>
);
import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from "react-admin";
import { LongForm } from "@react-admin/ra-form-layout";

const CustomerEdit = () => (
    <Edit>
        <LongForm>
            <LongForm.Section id="identity">
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </LongForm.Section>
            <LongForm.Section id="occupations" enableAccessControl>
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </LongForm.Section>
        </LongForm>
    </Edit>
);

loading

Used when enableAccessControl is set to true while checking for users permissions. Defaults to Loading from react-admin:

import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from 'react-admin';
import { LongForm } from '@react-admin/ra-form-layout';
import { Typography } from '@mui/material';

const CustomerEdit = () => (
    <Edit>
        <LongForm enableAccessControl>
            <LongForm.Section id="identity" loading={
                <Typography>
                    Loading your permissions...
                </Typography>
            }>
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </LongForm.Section>
            <LongForm.Section id="occupations" loading={
                <Typography>
                    Loading your permissions...
                </Typography>
            }>
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </LongForm.Section>
        </LongForm>
    </Edit>
);
import { ArrayInput, Edit, DateInput, SimpleFormIterator, TextInput } from "react-admin";
import { LongForm } from "@react-admin/ra-form-layout";
import { Typography } from "@mui/material";

const CustomerEdit = () => (
    <Edit>
        <LongForm enableAccessControl>
            <LongForm.Section id="identity" loading={<Typography>Loading your permissions...</Typography>}>
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
            </LongForm.Section>
            <LongForm.Section id="occupations" loading={<Typography>Loading your permissions...</Typography>}>
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </LongForm.Section>
        </LongForm>
    </Edit>
);

cardinality

The cardinality prop allows to specify a numeral quantity to be displayed next to the section label in the TOC.

LongForm.Section cardinality

import React, { useEffect, useState } from 'react';
import { Edit, TextField } from 'react-admin';

import { LongForm } from '@react-admin/ra-form-layout';

const CustomerEditWithCardinality = () => {
    const [publications, setPublications] = useState([]);
    useEffect(() => {
        setTimeout(() => {
            setPublications([
                { id: 1, title: 'Publication 1' },
                { id: 2, title: 'Publication 2' },
                { id: 3, title: 'Publication 3' },
            ]);
        }, 500);
    }, []);

    return (
        <Edit component="div">
            <LongForm>
                <LongForm.Section label="Identity">...</LongForm.Section>
                <LongForm.Section label="Occupations">...</LongForm.Section>
                <LongForm.Section label="Preferences">...</LongForm.Section>
                <LongForm.Section
                    label="Publications"
                    cardinality={publications.length}
                >
                    <ul>
                        {publications.map(publication => (
                            <li key={publication.id}>
                                <TextField
                                    source="title"
                                    record={publication}
                                />
                            </li>
                        ))}
                    </ul>
                </LongForm.Section>
            </LongForm>
        </Edit>
    );
};
import React, { useEffect, useState } from "react";
import { Edit, TextField } from "react-admin";

import { LongForm } from "@react-admin/ra-form-layout";

const CustomerEditWithCardinality = () => {
    const [publications, setPublications] = useState([]);
    useEffect(() => {
        setTimeout(() => {
            setPublications([
                { id: 1, title: "Publication 1" },
                { id: 2, title: "Publication 2" },
                { id: 3, title: "Publication 3" },
            ]);
        }, 500);
    }, []);

    return (
        <Edit component="div">
            <LongForm>
                <LongForm.Section label="Identity">...</LongForm.Section>
                <LongForm.Section label="Occupations">...</LongForm.Section>
                <LongForm.Section label="Preferences">...</LongForm.Section>
                <LongForm.Section label="Publications" cardinality={publications.length}>
                    <ul>
                        {publications.map((publication) => (
                            <li key={publication.id}>
                                <TextField source="title" record={publication} />
                            </li>
                        ))}
                    </ul>
                </LongForm.Section>
            </LongForm>
        </Edit>
    );
};

<CreateDialog>, <EditDialog> & <ShowDialog>

Sometimes it makes sense to edit or create a resource without leaving the context of the list page. For those cases, you can use the <CreateDialog>, <EditDialog> and <ShowDialog> components.

They accept a single child which is the form of:

  • either a <SimpleForm>, a <TabbedForm> or a custom one (just like the <Create> and <Edit> components) for <CreateDialog> and <EditDialog>
  • either a <SimpleShowLayout>, a <TabbedShowLayout> or a custom one (just like the <Show> component) for <ShowDialog>

Basic Usage, Based On Routing

By default, the Dialog components will use the Router's location to manage their state (open or closed).

This is the easiest way to integrate them in your React-Admin app, because you don't need to manage their state manually. You only need to add them inside your List component.

Here is an example:

import React from 'react';
import {
    List,
    Datagrid,
    SimpleForm,
    SimpleShowLayout,
    TextField,
    TextInput,
    DateInput,
    DateField,
    required,
    ShowButton,
} from 'react-admin';
import {
    EditDialog,
    CreateDialog,
    ShowDialog,
} from '@react-admin/ra-form-layout';

const CustomerList = () => (
    <>
        <List hasCreate>
            <Datagrid rowClick="edit">
                ...
                <ShowButton />
            </Datagrid>
        </List>
        <EditDialog>
            <SimpleForm>
                <TextField source="id" />
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput
                    source="date_of_birth"
                    label="born"
                    validate={required()}
                />
            </SimpleForm>
        </EditDialog>
        <CreateDialog>
            <SimpleForm>
                <TextField source="id" />
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput
                    source="date_of_birth"
                    label="born"
                    validate={required()}
                />
            </SimpleForm>
        </CreateDialog>
        <ShowDialog>
            <SimpleShowLayout>
                <TextField source="id" />
                <TextField source="first_name" />
                <TextField source="last_name" />
                <DateField source="date_of_birth" label="born" />
            </SimpleShowLayout>
        </ShowDialog>
    </>
);
import React from "react";
import {
    List,
    Datagrid,
    SimpleForm,
    SimpleShowLayout,
    TextField,
    TextInput,
    DateInput,
    DateField,
    required,
    ShowButton,
} from "react-admin";
import { EditDialog, CreateDialog, ShowDialog } from "@react-admin/ra-form-layout";

const CustomerList = () => (
    <>
        <List hasCreate>
            <Datagrid rowClick="edit">
                ...
                <ShowButton />
            </Datagrid>
        </List>
        <EditDialog>
            <SimpleForm>
                <TextField source="id" />
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput source="date_of_birth" label="born" validate={required()} />
            </SimpleForm>
        </EditDialog>
        <CreateDialog>
            <SimpleForm>
                <TextField source="id" />
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput source="date_of_birth" label="born" validate={required()} />
            </SimpleForm>
        </CreateDialog>
        <ShowDialog>
            <SimpleShowLayout>
                <TextField source="id" />
                <TextField source="first_name" />
                <TextField source="last_name" />
                <DateField source="date_of_birth" label="born" />
            </SimpleShowLayout>
        </ShowDialog>
    </>
);

Tip: In the example above, we added the hasCreate prop to the <List> component. This is necessary in order to display the "Create" button, because react-admin has no way to know that there exists a creation form for the "customer" resource otherwise.

Note: You can't use the <CreateDialog> and have a standard <Edit> specified on your <Resource>, because the <Routes> declarations would conflict. If you need this, use the <CreateInDialogButton> instead.

<CreateInDialogButton>, <EditInDialogButton> and <ShowInDialogButton>

In some cases, you might want to use these dialog components outside the List component. For instance, you might want to have an <Edit> view, including a <Datagrid>, for which you would like the ability to view, edit or add records using dialog components.

For this purpose, we also provide <CreateInDialogButton>, <EditInDialogButton> and <ShowInDialogButton>.

These components will create a dialog component (<CreateDialog>, <EditDialog> or <ShowDialog> respectively), along with a <Button> to open them.

These components are also responsible for creating a <FormDialogContext>, used to manage the dialog's state (open or closed), inside which the dialog component will render.

Here is an implementation example:

import React, { ReactNode } from 'react';
import {
    Datagrid,
    DateField,
    DateInput,
    Edit,
    ReferenceManyField,
    required,
    SelectField,
    SelectInput,
    SimpleForm,
    SimpleShowLayout,
    TextField,
    TextInput,
    useRecordContext,
} from 'react-admin';
import {
    CreateInDialogButton,
    EditInDialogButton,
    ShowInDialogButton,
} from '@react-admin/ra-form-layout';

const sexChoices = [
    { id: 'male', name: 'Male' },
    { id: 'female', name: 'Female' },
];

const CustomerForm = (props: any) => (
    <SimpleForm defaultValues={{ firstname: 'John', name: 'Doe' }} {...props}>
        <TextInput source="first_name" validate={required()} />
        <TextInput source="last_name" validate={required()} />
        <DateInput source="dob" label="born" validate={required()} />
        <SelectInput source="sex" choices={sexChoices} />
    </SimpleForm>
);

const CustomerLayout = (props: any) => (
    <SimpleShowLayout {...props}>
        <TextField source="first_name" />
        <TextField source="last_name" />
        <DateField source="dob" label="born" />
        <SelectField source="sex" choices={sexChoices} />
    </SimpleShowLayout>
);

// helper component to add actions buttons in a column (children),
// and also in the header (label) of a Datagrid
const DatagridActionsColumn = ({
    label,
    children,
}: {
    label: ReactNode;
    children: ReactNode;
}) => <>{children}</>;

const NestedCustomersDatagrid = () => {
    const record = useRecordContext();

    const createButton = (
        <CreateInDialogButton
            inline
            fullWidth
            maxWidth="md"
            record={{ employer_id: record?.id }} // pre-populates the employer_id to link the new customer to the current employer
        >
            <CustomerForm />
        </CreateInDialogButton>
    );

    const editButton = (
        <EditInDialogButton fullWidth maxWidth="md">
            <CustomerForm />
        </EditInDialogButton>
    );

    const showButton = (
        <ShowInDialogButton fullWidth maxWidth="md">
            <CustomerLayout />
        </ShowInDialogButton>
    );

    return (
        <ReferenceManyField
            label="Customers"
            reference="customers"
            target="employer_id"
        >
            <Datagrid>
                <TextField source="id" />
                <TextField source="first_name" />
                <TextField source="last_name" />
                <DateField source="dob" label="born" />
                <SelectField source="sex" choices={sexChoices} />
                {/* Using a component as label is a trick to render it in the Datagrid header */}
                <DatagridActionsColumn label={createButton}>
                    {editButton}
                    {showButton}
                </DatagridActionsColumn>
            </Datagrid>
        </ReferenceManyField>
    );
};

const EmployerEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput source="name" validate={required()} />
            <TextInput source="address" validate={required()} />
            <TextInput source="city" validate={required()} />
            <NestedCustomersDatagrid />
        </SimpleForm>
    </Edit>
);
import React from "react";
import {
    Datagrid,
    DateField,
    DateInput,
    Edit,
    ReferenceManyField,
    required,
    SelectField,
    SelectInput,
    SimpleForm,
    SimpleShowLayout,
    TextField,
    TextInput,
    useRecordContext,
} from "react-admin";
import { CreateInDialogButton, EditInDialogButton, ShowInDialogButton } from "@react-admin/ra-form-layout";

const sexChoices = [
    { id: "male", name: "Male" },
    { id: "female", name: "Female" },
];

const CustomerForm = (props) => (
    <SimpleForm defaultValues={{ firstname: "John", name: "Doe" }} {...props}>
        <TextInput source="first_name" validate={required()} />
        <TextInput source="last_name" validate={required()} />
        <DateInput source="dob" label="born" validate={required()} />
        <SelectInput source="sex" choices={sexChoices} />
    </SimpleForm>
);

const CustomerLayout = (props) => (
    <SimpleShowLayout {...props}>
        <TextField source="first_name" />
        <TextField source="last_name" />
        <DateField source="dob" label="born" />
        <SelectField source="sex" choices={sexChoices} />
    </SimpleShowLayout>
);

// helper component to add actions buttons in a column (children),
// and also in the header (label) of a Datagrid
const DatagridActionsColumn = ({ label, children }) => <>{children}</>;

const NestedCustomersDatagrid = () => {
    const record = useRecordContext();

    const createButton = (
        <CreateInDialogButton
            inline
            fullWidth
            maxWidth="md"
            record={{ employer_id: record?.id }} // pre-populates the employer_id to link the new customer to the current employer
        >
            <CustomerForm />
        </CreateInDialogButton>
    );

    const editButton = (
        <EditInDialogButton fullWidth maxWidth="md">
            <CustomerForm />
        </EditInDialogButton>
    );

    const showButton = (
        <ShowInDialogButton fullWidth maxWidth="md">
            <CustomerLayout />
        </ShowInDialogButton>
    );

    return (
        <ReferenceManyField label="Customers" reference="customers" target="employer_id">
            <Datagrid>
                <TextField source="id" />
                <TextField source="first_name" />
                <TextField source="last_name" />
                <DateField source="dob" label="born" />
                <SelectField source="sex" choices={sexChoices} />
                {/* Using a component as label is a trick to render it in the Datagrid header */}
                <DatagridActionsColumn label={createButton}>
                    {editButton}
                    {showButton}
                </DatagridActionsColumn>
            </Datagrid>
        </ReferenceManyField>
    );
};

const EmployerEdit = () => (
    <Edit>
        <SimpleForm>
            <TextInput source="name" validate={required()} />
            <TextInput source="address" validate={required()} />
            <TextInput source="city" validate={required()} />
            <NestedCustomersDatagrid />
        </SimpleForm>
    </Edit>
);

These components accept the following props:

  • inline: set to true to display only an MUI <IconButton> instead of the full <Button>. The label will still be available as a <Tooltip> though.
  • icon: allows to override the default icon.
  • label: allows to override the default button label. I18N is supported.
  • ButtonProps: object containing props to pass to MUI's <Button>.
  • remaining props will be passed to the corresponding dialog component (<CreateDialog>, <EditDialog> or <ShowDialog>).

Standalone Usage

<CreateDialog>, <EditDialog> and <ShowDialog> also offer the ability to work standalone, without using the Router's location.

To allow for standalone usage, they require the following props:

  • isOpen: a boolean holding the open/close state
  • open: a function that will be called when a component needs to open the dialog (e.g. a button)
  • close: a function that will be called when a component needs to close the dialog (e.g. the dialog's close button)

Tip: These props are exactly the same as what is stored inside a FormDialogContext. This means that you can also rather provide your own FormDialogContext with these values, and render your dialog component inside it, to activate standalone mode.

Below is an example of an <Edit> page, including a 'create a new customer' button, that opens a fully controlled <CreateDialog>.

import React, { useCallback, useState } from 'react';
import {
    Button,
    Datagrid,
    DateField,
    DateInput,
    Edit,
    ReferenceManyField,
    required,
    SelectField,
    SelectInput,
    SimpleForm,
    TextField,
    TextInput,
    useRecordContext,
} from 'react-admin';
import { CreateDialog } from '@react-admin/ra-form-layout';

const sexChoices = [
    { id: 'male', name: 'Male' },
    { id: 'female', name: 'Female' },
];

const CustomerForm = (props: any) => (
    <SimpleForm defaultValues={{ firstname: 'John', name: 'Doe' }} {...props}>
        <TextInput source="first_name" validate={required()} />
        <TextInput source="last_name" validate={required()} />
        <DateInput source="dob" label="born" validate={required()} />
        <SelectInput source="sex" choices={sexChoices} />
    </SimpleForm>
);

const EmployerSimpleFormWithFullyControlledDialogs = () => {
    const record = useRecordContext();

    const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
    const openCreateDialog = useCallback(() => {
        setIsCreateDialogOpen(true);
    }, []);
    const closeCreateDialog = useCallback(() => {
        setIsCreateDialogOpen(false);
    }, []);

    return (
        <SimpleForm>
            <TextInput source="name" validate={required()} />
            <TextInput source="address" validate={required()} />
            <TextInput source="city" validate={required()} />
            <Button
                label="Create a new customer"
                onClick={() => openCreateDialog()}
                size="medium"
                variant="contained"
                sx={{ mb: 4 }}
            />
            <CreateDialog
                fullWidth
                maxWidth="md"
                record={{ employer_id: record?.id }} // pre-populates the employer_id to link the new customer to the current employer
                isOpen={isCreateDialogOpen}
                open={openCreateDialog}
                close={closeCreateDialog}
                resource="customers"
            >
                <CustomerForm />
            </CreateDialog>
            <ReferenceManyField
                label="Customers"
                reference="customers"
                target="employer_id"
            >
                <Datagrid>
                    <TextField source="id" />
                    <TextField source="first_name" />
                    <TextField source="last_name" />
                    <DateField source="dob" label="born" />
                    <SelectField source="sex" choices={sexChoices} />
                </Datagrid>
            </ReferenceManyField>
        </SimpleForm>
    );
};

const EmployerEdit = () => (
    <Edit>
        <EmployerSimpleFormWithFullyControlledDialogs />
    </Edit>
);
import React, { useCallback, useState } from "react";
import {
    Button,
    Datagrid,
    DateField,
    DateInput,
    Edit,
    ReferenceManyField,
    required,
    SelectField,
    SelectInput,
    SimpleForm,
    TextField,
    TextInput,
    useRecordContext,
} from "react-admin";
import { CreateDialog } from "@react-admin/ra-form-layout";

const sexChoices = [
    { id: "male", name: "Male" },
    { id: "female", name: "Female" },
];

const CustomerForm = (props) => (
    <SimpleForm defaultValues={{ firstname: "John", name: "Doe" }} {...props}>
        <TextInput source="first_name" validate={required()} />
        <TextInput source="last_name" validate={required()} />
        <DateInput source="dob" label="born" validate={required()} />
        <SelectInput source="sex" choices={sexChoices} />
    </SimpleForm>
);

const EmployerSimpleFormWithFullyControlledDialogs = () => {
    const record = useRecordContext();

    const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
    const openCreateDialog = useCallback(() => {
        setIsCreateDialogOpen(true);
    }, []);
    const closeCreateDialog = useCallback(() => {
        setIsCreateDialogOpen(false);
    }, []);

    return (
        <SimpleForm>
            <TextInput source="name" validate={required()} />
            <TextInput source="address" validate={required()} />
            <TextInput source="city" validate={required()} />
            <Button
                label="Create a new customer"
                onClick={() => openCreateDialog()}
                size="medium"
                variant="contained"
                sx={{ mb: 4 }}
            />
            <CreateDialog
                fullWidth
                maxWidth="md"
                record={{ employer_id: record?.id }} // pre-populates the employer_id to link the new customer to the current employer
                isOpen={isCreateDialogOpen}
                open={openCreateDialog}
                close={closeCreateDialog}
                resource="customers"
            >
                <CustomerForm />
            </CreateDialog>
            <ReferenceManyField label="Customers" reference="customers" target="employer_id">
                <Datagrid>
                    <TextField source="id" />
                    <TextField source="first_name" />
                    <TextField source="last_name" />
                    <DateField source="dob" label="born" />
                    <SelectField source="sex" choices={sexChoices} />
                </Datagrid>
            </ReferenceManyField>
        </SimpleForm>
    );
};

const EmployerEdit = () => (
    <Edit>
        <EmployerSimpleFormWithFullyControlledDialogs />
    </Edit>
);

title

Unlike the <Create>, <Edit> and <Show> components, with Dialog components the title will be displayed in the <Dialog>, not in the <AppBar>.

Still, for <EditDialog> and <ShowDialog>, if you pass a custom title component, it will render in the same RecordContext as the dialog's child component. That means you can display non-editable details of the current record in the title component.

Here is an example:

import React from 'react';
import {
    List,
    Datagrid,
    SimpleForm,
    SimpleShowLayout,
    TextField,
    TextInput,
    DateInput,
    DateField,
    required,
    ShowButton,
    useRecordContext,
} from 'react-admin';
import {
    EditDialog,
    CreateDialog,
    ShowDialog,
} from '@react-admin/ra-form-layout';

const CustomerEditTitle = () => {
    const record = useRecordContext();
    return record ? (
        <span>
            Edit {record.last_name} {record.first_name}
        </span>
    ) : null;
};

const CustomerShowTitle = () => {
    const record = useRecordContext();
    return record ? (
        <span>
            Show {record.last_name} {record.first_name}
        </span>
    ) : null;
};

const CustomerList = () => (
    <>
        <List hasCreate>
            <Datagrid rowClick="edit">
                ...
                <ShowButton />
            </Datagrid>
        </List>
        <EditDialog title={<CustomerEditTitle />}>
            <SimpleForm>
                <TextField source="id" />
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput
                    source="date_of_birth"
                    label="born"
                    validate={required()}
                />
            </SimpleForm>
        </EditDialog>
        <CreateDialog title="Create a new customer">
            <SimpleForm>
                <TextField source="id" />
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput
                    source="date_of_birth"
                    label="born"
                    validate={required()}
                />
            </SimpleForm>
        </CreateDialog>
        <ShowDialog title={<CustomerShowTitle />}>
            <SimpleShowLayout>
                <TextField source="id" />
                <TextField source="first_name" />
                <TextField source="last_name" />
                <DateField source="date_of_birth" label="born" />
            </SimpleShowLayout>
        </ShowDialog>
    </>
);
import React from "react";
import {
    List,
    Datagrid,
    SimpleForm,
    SimpleShowLayout,
    TextField,
    TextInput,
    DateInput,
    DateField,
    required,
    ShowButton,
    useRecordContext,
} from "react-admin";
import { EditDialog, CreateDialog, ShowDialog } from "@react-admin/ra-form-layout";

const CustomerEditTitle = () => {
    const record = useRecordContext();
    return record ? (
        <span>
            Edit {record.last_name} {record.first_name}
        </span>
    ) : null;
};

const CustomerShowTitle = () => {
    const record = useRecordContext();
    return record ? (
        <span>
            Show {record.last_name} {record.first_name}
        </span>
    ) : null;
};

const CustomerList = () => (
    <>
        <List hasCreate>
            <Datagrid rowClick="edit">
                ...
                <ShowButton />
            </Datagrid>
        </List>
        <EditDialog title={<CustomerEditTitle />}>
            <SimpleForm>
                <TextField source="id" />
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput source="date_of_birth" label="born" validate={required()} />
            </SimpleForm>
        </EditDialog>
        <CreateDialog title="Create a new customer">
            <SimpleForm>
                <TextField source="id" />
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput source="date_of_birth" label="born" validate={required()} />
            </SimpleForm>
        </CreateDialog>
        <ShowDialog title={<CustomerShowTitle />}>
            <SimpleShowLayout>
                <TextField source="id" />
                <TextField source="first_name" />
                <TextField source="last_name" />
                <DateField source="date_of_birth" label="born" />
            </SimpleShowLayout>
        </ShowDialog>
    </>
);

Customizing The Dialog

You can also pass the props accepted by the MUI <Dialog> component, like fullWidth or maxWidth, directly to <CreateDialog>, <EditDialog> or <ShowDialog>.

import React from 'react';
import {
    List,
    Datagrid,
    SimpleForm,
    SimpleShowLayout,
    TextField,
    TextInput,
    DateInput,
    DateField,
    required,
    ShowButton,
} from 'react-admin';
import {
    EditDialog,
    CreateDialog,
    ShowDialog,
} from '@react-admin/ra-form-layout';

const CustomerList = () => (
    <>
        <List hasCreate>
            <Datagrid rowClick="edit">
                ...
                <ShowButton />
            </Datagrid>
        </List>
        <EditDialog fullWidth maxWidth="md">
            <SimpleForm>
                <TextField source="id" />
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput
                    source="date_of_birth"
                    label="born"
                    validate={required()}
                />
            </SimpleForm>
        </EditDialog>
        <CreateDialog fullWidth maxWidth="md">
            <SimpleForm>
                <TextField source="id" />
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput
                    source="date_of_birth"
                    label="born"
                    validate={required()}
                />
            </SimpleForm>
        </CreateDialog>
        <ShowDialog fullWidth maxWidth="md">
            <SimpleShowLayout>
                <TextField source="id" />
                <TextField source="first_name" />
                <TextField source="last_name" />
                <DateField source="date_of_birth" label="born" />
            </SimpleShowLayout>
        </ShowDialog>
    </>
);
import React from "react";
import {
    List,
    Datagrid,
    SimpleForm,
    SimpleShowLayout,
    TextField,
    TextInput,
    DateInput,
    DateField,
    required,
    ShowButton,
} from "react-admin";
import { EditDialog, CreateDialog, ShowDialog } from "@react-admin/ra-form-layout";

const CustomerList = () => (
    <>
        <List hasCreate>
            <Datagrid rowClick="edit">
                ...
                <ShowButton />
            </Datagrid>
        </List>
        <EditDialog fullWidth maxWidth="md">
            <SimpleForm>
                <TextField source="id" />
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput source="date_of_birth" label="born" validate={required()} />
            </SimpleForm>
        </EditDialog>
        <CreateDialog fullWidth maxWidth="md">
            <SimpleForm>
                <TextField source="id" />
                <TextInput source="first_name" validate={required()} />
                <TextInput source="last_name" validate={required()} />
                <DateInput source="date_of_birth" label="born" validate={required()} />
            </SimpleForm>
        </CreateDialog>
        <ShowDialog fullWidth maxWidth="md">
            <SimpleShowLayout>
                <TextField source="id" />
                <TextField source="first_name" />
                <TextField source="last_name" />
                <DateField source="date_of_birth" label="born" />
            </SimpleShowLayout>
        </ShowDialog>
    </>
);

Redirection After Deletion

If you use <SimpleForm> as child of <EditDialog> or <EditInDialogButton>, the default form toolbar includes a <DeleteButton>. And upon deletion, this button redirects to the current resource list. This is probably not what you want, so it's common to customize the form toolbar to disable the redirection after deletion:

// src/CustomToolbar.tsx
import { Toolbar, SaveButton, DeleteButton } from 'react-admin';

export const CustomToolbar = () => (
    <Toolbar sx={{ justifyContent: 'space-between' }}>
        <SaveButton />
        <DeleteButton redirect={false} />
    </Toolbar>
);

// src/EmployerEdit.tsx
import { Edit, SimpleForm, TextInput, ReferenceManyField } from 'react-admin';
import {