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 <AccordionFormPanel> 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, AccordionFormPanel } 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>
            <AccordionFormPanel 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} />
            </AccordionFormPanel>
            <AccordionFormPanel label="Occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </AccordionFormPanel>
            <AccordionFormPanel label="Preferences">
                <SelectInput
                    source="language"
                    choices={languageChoices}
                    defaultValue="en"
                />
                <BooleanInput source="dark_theme" />
                <BooleanInput source="accepts_emails_from_partners" />
            </AccordionFormPanel>
        </AccordionForm>
    </Edit>
);
import {
    Edit,
    TextField,
    TextInput,
    DateInput,
    SelectInput,
    ArrayInput,
    SimpleFormIterator,
    BooleanInput,
} from 'react-admin';
import { AccordionForm, AccordionFormPanel } 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>
            <AccordionFormPanel 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} />
            </AccordionFormPanel>
            <AccordionFormPanel label="Occupations">
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </AccordionFormPanel>
            <AccordionFormPanel label="Preferences">
                <SelectInput
                    source="language"
                    choices={languageChoices}
                    defaultValue="en"
                />
                <BooleanInput source="dark_theme" />
                <BooleanInput source="accepts_emails_from_partners" />
            </AccordionFormPanel>
        </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, AccordionFormPanel } 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>
            <AccordionFormPanel label="Identity" defaultExpanded>
                <TextField source="id" />
                ...

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.

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

<AccordionFormPanel>

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

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

Prop Required Type Default Description
label Required string or ReactNode - The main label used as the accordion summary. Appears in red when the accordion has errors
id Optional string - An id for this Accordion to be used in the useFormGroup hook and for CSS classes.
children Required ReactNode - A list of <Input> elements
count Optional ReactNode - A number to be displayed next to the summary, to quantify it
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 (except if autoClose = true on the parent)
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.
sx Optional Object - An object containing the MUI style overrides to apply to the root component.
import {
    Edit,
    TextField,
    TextInput,
    DateInput,
    SelectInput,
    ArrayInput,
    SimpleFormIterator,
    BooleanInput,
} from 'react-admin';

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

const CustomerEdit = () => (
    <Edit component="div">
        <AccordionForm>
            <AccordionFormPanel 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} />
            </AccordionFormPanel>
        </AccordionForm>
    </Edit>
);
import {
    Edit,
    TextField,
    TextInput,
    DateInput,
    SelectInput,
    ArrayInput,
    SimpleFormIterator,
    BooleanInput,
} from 'react-admin';

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

const CustomerEdit = () => (
    <Edit component="div">
        <AccordionForm>
            <AccordionFormPanel 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} />
            </AccordionFormPanel>
        </AccordionForm>
    </Edit>
);

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

<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
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
label Required string or ReactNode - The main label used as the accordion summary.
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 false If true, the Accordion take sthe 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, AccordionFormPanel } 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" fullWidth>
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </AccordionSection>
            <AccordionSection label="Preferences" fullWidth>
                <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';

import { AccordionForm, AccordionFormPanel } 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" fullWidth>
                <ArrayInput source="occupations" label="">
                    <SimpleFormIterator>
                        <TextInput source="name" validate={required()} />
                        <DateInput source="from" validate={required()} />
                        <DateInput source="to" />
                    </SimpleFormIterator>
                </ArrayInput>
            </AccordionSection>
            <AccordionSection label="Preferences" fullWidth>
                <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, WizardFormStep } from '@react-admin/ra-form-layout';

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

const PostCreate = () => (
    <Create>
        <WizardForm>
            <WizardFormStep label="First step">
                <TextInput source="title" validate={required()} />
            </WizardFormStep>
            <WizardFormStep label="Second step">
                <TextInput source="description" />
            </WizardFormStep>
            <WizardFormStep label="Third step">
                <TextInput source="fullDescription" validate={required()} />
            </WizardFormStep>
        </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 <WizardFormStep> component accepts a translation key:

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

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

const PostCreate = () => (
    <Create>
        <WizardForm>
            <WizardFormStep label="myapp.posts.steps.general">
                <TextInput source="title" validate={required()} />
            </WizardFormStep>
            <WizardFormStep label="myapp.posts.steps.description">
                <TextInput source="description" />
            </WizardFormStep>
            <WizardFormStep label="myapp.posts.steps.misc">
                <TextInput source="fullDescription" validate={required()} />
            </WizardFormStep>
        </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,
    WizardFormStep,
} 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 />}>
            <WizardFormStep label="First step">
                <TextInput source="title" validate={required()} />
            </WizardFormStep>
            <WizardFormStep label="Second step">
                <TextInput source="description" />
            </WizardFormStep>
            <WizardFormStep label="Third step">
                <TextInput source="fullDescription" validate={required()} />
            </WizardFormStep>
        </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,
    WizardFormStep,
} 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 />}>
            <WizardFormStep label="First step">
                <TextInput source="title" validate={required()} />
            </WizardFormStep>
            <WizardFormStep label="Second step">
                <TextInput source="description" />
            </WizardFormStep>
            <WizardFormStep label="Third step">
                <TextInput source="fullDescription" validate={required()} />
            </WizardFormStep>
        </WizardForm>
    </Create>
);

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,
    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>
);

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

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>
            <WizardFormStep label="First step">
                <TextInput source="title" validate={required()} />
            </WizardFormStep>
            <WizardFormStep label="Second step">
                <TextInput source="description" />
            </WizardFormStep>
            <WizardFormStep label="Third step">
                <TextInput source="fullDescription" validate={required()} />
            </WizardFormStep>
            <WizardFormStep label="">
                <FinalStepContent />
            </WizardFormStep>
        </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>
            <WizardFormStep label="First step">
                <TextInput source="title" validate={required()} />
            </WizardFormStep>
            <WizardFormStep label="Second step">
                <TextInput source="description" />
            </WizardFormStep>
            <WizardFormStep label="Third step">
                <TextInput source="fullDescription" validate={required()} />
            </WizardFormStep>
            <WizardFormStep label="">
                <FinalStepContent />
            </WizardFormStep>
        </WizardForm>
    </Create>
);

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

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
label Required string - The main label used as the section title. Appears in red when the section has errors
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

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()} fullWidth />
        <TextInput source="last_name" validate={required()} fullWidth />
        <DateInput source="dob" label="born" validate={required()} fullWidth />
        <SelectInput source="sex" choices={sexChoices} fullWidth />
    </SimpleForm>
);

const CustomerLayout = (props: any) => (
    <SimpleShowLayout {...props}>
        <TextField source="first_name" fullWidth />
        <TextField source="last_name" fullWidth />
        <DateField source="dob" label="born" fullWidth />
        <SelectField source="sex" choices={sexChoices} fullWidth />
    </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, { 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()} fullWidth />
        <TextInput source="last_name" validate={required()} fullWidth />
        <DateInput source="dob" label="born" validate={required()} fullWidth />
        <SelectInput source="sex" choices={sexChoices} fullWidth />
    </SimpleForm>
);

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

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()} fullWidth />
        <TextInput source="last_name" validate={required()} fullWidth />
        <DateInput source="dob" label="born" validate={required()} fullWidth />
        <SelectInput source="sex" choices={sexChoices} fullWidth />
    </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: any) => (
    <SimpleForm defaultValues={{ firstname: 'John', name: 'Doe' }} {...props}>
        <TextInput source="first_name" validate={required()} fullWidth />
        <TextInput source="last_name" validate={required()} fullWidth />
        <DateInput source="dob" label="born" validate={required()} fullWidth />
        <SelectInput source="sex" choices={sexChoices} fullWidth />
    </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 { EditInDialogButton } from '@react-admin/ra-form-layout';
import { CustomToolbar } from './CustomToolbar';

const EmployerEdit = () => (
    <Edit>
        <SimpleForm>
            ...
            <ReferenceManyField target="employer_id" reference="customers">
                <Datagrid>
                    ...
                    <EditInDialogButton fullWidth maxWidth="sm">
                        <SimpleForm toolbar={<CustomToolbar />}>
                            <TextInput source="first_name" />
                            <TextInput source="last_name" />
                        </SimpleForm>
                    </EditInDialogButton>
                </Datagrid>
            </ReferenceManyField>
        </SimpleForm>
    </Edit>
);
// 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 { EditInDialogButton } from '@react-admin/ra-form-layout';
import { CustomToolbar } from './CustomToolbar';

const EmployerEdit = () => (
    <Edit>
        <SimpleForm>
            ...
            <ReferenceManyField target="employer_id" reference="customers">
                <Datagrid>
                    ...
                    <EditInDialogButton fullWidth maxWidth="sm">
                        <SimpleForm toolbar={<CustomToolbar />}>
                            <TextInput source="first_name" />
                            <TextInput source="last_name" />
                        </SimpleForm>
                    </EditInDialogButton>
                </Datagrid>
            </ReferenceManyField>
        </SimpleForm>
    </Edit>
);

<StackedFilters>

<StackedFilters> is an alternative filter component for <List>. It introduces the concept of operators to allow richer filtering.

Usage

import {
    BooleanField,
    CreateButton,
    Datagrid,
    List,
    NumberField,
    ReferenceArrayField,
    TextField,
    TopToolbar,
} from 'react-admin';
import {
    StackedFilters,
    FiltersConfig,
    textFilter,
    numberFilter,
    referenceFilter,
    booleanFilter,
} from '@react-admin/ra-form-layout';

const postListFilters: FiltersConfig = {
    title: textFilter(),
    views: numberFilter(),
    tag_ids: referenceFilter({ reference: 'tags' }),
    published: booleanFilter(),
};

const PostListToolbar = () => (
    <TopToolbar>
        <CreateButton />
        <StackedFilters config={postListFilters} />
    </TopToolbar>
);

const PostList = () => (
    <List actions={<PostListToolbar />}>
        <Datagrid>
            <TextField source="title" />
            <NumberField source="views" />
            <ReferenceArrayField tags="tags" source="tag_ids" />
            <BooleanField source="published" />
        </Datagrid>
    </List>
);
import {
    BooleanField,
    CreateButton,
    Datagrid,
    List,
    NumberField,
    ReferenceArrayField,
    TextField,
    TopToolbar,
} from 'react-admin';
import {
    StackedFilters,
    FiltersConfig,
    textFilter,
    numberFilter,
    referenceFilter,
    booleanFilter,
} from '@react-admin/ra-form-layout';

const postListFilters: FiltersConfig = {
    title: textFilter(),
    views: numberFilter(),
    tag_ids: referenceFilter({ reference: 'tags' }),
    published: booleanFilter(),
};

const PostListToolbar = () => (
    <TopToolbar>
        <CreateButton />
        <StackedFilters config={postListFilters} />
    </TopToolbar>
);

const PostList = () => (
    <List actions={<PostListToolbar />}>
        <Datagrid>
            <TextField source="title" />
            <NumberField source="views" />
            <ReferenceArrayField tags="tags" source="tag_ids" />
            <BooleanField source="published" />
        </Datagrid>
    </List>
);

You must also update your data provider to support filters with operators. See the data provider configuration section below.

Filters Configuration

<StackedFilters> and its underlying component, <StackedFiltersForm> needs a filter configuration. This is an object defining the operators and UI for each source that can be used as a filter.

It looks like this:

import { FiltersConfig } from '@react-admin/ra-form-layout';
import { NumberInput } from 'react-admin';
import { MyNumberRangeInput } from './MyNumberRangeInput';

const postListFilters: FiltersConfig = {
    views: {
        operators: [
            { value: 'eq', label: 'Equals' },
            { value: 'neq', label: 'Not Equals' },
            {
                value: 'between',
                label: 'Between',
                input: ({ source }) => <MyNumberRangeInput source={source} />,
            },
        ],
        input: ({ source }) => <NumberInput source={source} />,
    },
};
import { FiltersConfig } from '@react-admin/ra-form-layout';
import { NumberInput } from 'react-admin';
import { MyNumberRangeInput } from './MyNumberRangeInput';

const postListFilters: FiltersConfig = {
    views: {
        operators: [
            { value: 'eq', label: 'Equals' },
            { value: 'neq', label: 'Not Equals' },
            {
                value: 'between',
                label: 'Between',
                input: ({ source }) => <MyNumberRangeInput source={source} />,
            },
        ],
        input: ({ source }) => <NumberInput source={source} />,
    },
};

As you can see, the source is the config object key. It contains an array of operators and a default input, used for operators that don't define their own.

An operator is an object that has a label and a value. The label can be a translation key. The value will be used as a suffix to the source and passed to the list filters. For instance, with the source views, the operator eq and value set to 0 using the NumberInput, the dataProvider will receive the following filter:

{
    views_eq: 0;
}

Besides, any operator can provide its own input if it needs.

Filter Configuration Builders

To make it easier to create a filter configuration, we provide some helper functions. Each of them has predefined operators and inputs. They accept an array of operators if you want to remove some of them.

  • textFilter: A filter for text fields. Defines the following operator: eq, neq and q.
  • numberFilter: A filter for number fields. Defines the following operator: eq, neq, lt and gt.
  • dateFilter: A filter for date fields. Defines the following operator: eq, neq, lt and gt.
  • booleanFilter: A filter for boolean fields. Defines the following operator: eq.
  • choicesFilter: A filter for fields that accept a value from a list of choices. Defines the following operator: eq, neq, eq_any and neq_any.
  • choicesArrayFilter: A filter for array fields. Defines the following operator: inc, inc_any and ninc_any.
  • referenceFilter: A filter for reference fields. Defines the following operator: eq, neq, eq_any and neq_any.

Build your filter configuration by calling the helpers for each source:

import {
    FiltersConfig,
    textFilter,
    numberFilter,
    referenceFilter,
    booleanFilter,
} from '@react-admin/ra-form-layout';

const postListFilters: FiltersConfig = {
    title: textFilter(),
    views: numberFilter(),
    tag_ids: referenceFilter({ reference: 'tags' }),
    published: booleanFilter(),
};
import {
    FiltersConfig,
    textFilter,
    numberFilter,
    referenceFilter,
    booleanFilter,
} from '@react-admin/ra-form-layout';

const postListFilters: FiltersConfig = {
    title: textFilter(),
    views: numberFilter(),
    tag_ids: referenceFilter({ reference: 'tags' }),
    published: booleanFilter(),
};

Data Provider Configuration

In react-admin, dataProvider.getList() accepts a filter parameter to filter the records. There is no notion of operators in this parameter, as the expected format is an object like { field: value }. As StackedFilters needs operators, it uses a convention to concatenate the field name and the operator with an underscore.

For instance, if the Post resource has a title field, and you configure <StackedFilters> to allow filtering on this field as a text field, the dataProvider.getList() may receive the following filter parameter:

  • title_eq
  • title_neq
  • title_q

The actual suffixes depend on the type of filter configured in <StackedFilter> (see filters configuration builders above). Here is an typical call to dataProvider.getList() with a posts list using <StackedFilters>:

const { data } = useGetList('posts', {
    filter: {
        title_q: 'lorem',
        date_gte: '2021-01-01',
        views_eq: 0,
        tags_inc_any: [1, 2],
    },
    pagination: { page: 1, perPage: 10 },
    sort: { field: 'title', order: 'ASC' },
});

It's up to your data provider to convert the filter parameter into a query that your API understands.

For instance, if your API expects filters as an array of criteria objects ([{ field, operator, value }]), dataProvider.getList() should convert the filter parameter as follows:

const dataProvider = {
    // ...
    getList: async (resource, params) => {
        const { filter } = params;
        const filterFields = Object.keys(filter);
        const criteria = [];
        // eq operator
        filterFields.filter(field => field.endsWith('_eq')).forEach(field => {
            criteria.push({ field: field.replace('_eq', ''), operator: 'eq', value: filter[field] });
        });
        // neq operator
        filterFields.filter(field => field.endsWith('_neq')).forEach(field => {
            criteria.push({ field: field.replace('_neq', ''), operator: 'neq', value: filter[field] });
        });
        // q operator
        filterFields.filter(field => field.endsWith('_q')).forEach(field => {
            criteria.push({ field: field.replace('_q', ''), operator: 'q', value: filter[field] });
        });
        // ...
    },
}
const dataProvider = {
    // ...
    getList: async (resource, params) => {
        const { filter } = params;
        const filterFields = Object.keys(filter);
        const criteria = [];
        // eq operator
        filterFields.filter(field => field.endsWith('_eq')).forEach(field => {
            criteria.push({ field: field.replace('_eq', ''), operator: 'eq', value: filter[field] });
        });
        // neq operator
        filterFields.filter(field => field.endsWith('_neq')).forEach(field => {
            criteria.push({ field: field.replace('_neq', ''), operator: 'neq', value: filter[field] });
        });
        // q operator
        filterFields.filter(field => field.endsWith('_q')).forEach(field => {
            criteria.push({ field: field.replace('_q', ''), operator: 'q', value: filter[field] });
        });
        // ...
    },
}

Few of the existing data providers implement this convention. this means you'll probably have to adapt your data provider to support the operators used by <StackedFilters>.

Internationalization

The source field names are translatable. ra-form-layout uses the react-admin resource and field name translation system. This is an example of an English translation file:

// in i18n/en.js

export default {
    resources: {
        customer: {
            name: 'Customer |||| Customers',
            fields: {
                first_name: 'First name',
                last_name: 'Last name',
                dob: 'Date of birth',
            },
        },
    },
};
// in i18n/en.js

export default {
    resources: {
        customer: {
            name: 'Customer |||| Customers',
            fields: {
                first_name: 'First name',
                last_name: 'Last name',
                dob: 'Date of birth',
            },
        },
    },
};

ra-form-layout also supports internationalization for operators. To leverage it, pass a translation key as the operator label:

import { FiltersConfig } from '@react-admin/ra-form-layout';
import DateRangeInput from './DateRangeInput';

const MyFilterConfig: FiltersConfig = {
    published_at: {
        operators: [
            {
                value: 'between',
                label: 'resources.posts.filters.operators.between',
            },
            {
                value: 'nbetween',
                label: 'resources.posts.filters.operators.nbetween',
            },
        ],
        input: ({ source }) => <DateRangeInput source={source} />,
    },
};
import { FiltersConfig } from '@react-admin/ra-form-layout';
import DateRangeInput from './DateRangeInput';

const MyFilterConfig: FiltersConfig = {
    published_at: {
        operators: [
            {
                value: 'between',
                label: 'resources.posts.filters.operators.between',
            },
            {
                value: 'nbetween',
                label: 'resources.posts.filters.operators.nbetween',
            },
        ],
        input: ({ source }) => <DateRangeInput source={source} />,
    },
};

<StackedFilters>

This component is responsible for showing the Filters button that displays the filtering form inside a MUI Popover. It must be given the filtering configuration through its config prop.

import {
    BooleanField,
    CreateButton,
    Datagrid,
    List,
    NumberField,
    ReferenceArrayField,
    TopToolbar,
    TextField,
} from 'react-admin';
import {
    StackedFilters,
    FiltersConfig,
    textFilter,
    numberFilter,
    referenceFilter,
    booleanFilter,
} from '@react-admin/ra-form-layout';

const postListFilters: FiltersConfig = {
    title: textFilter(),
    views: numberFilter(),
    tag_ids: referenceFilter({ reference: 'tags' }),
    published: booleanFilter(),
};

const PostListToolbar = () => (
    <TopToolbar>
        <CreateButton />
        <StackedFilters config={postListFilters} />
    </TopToolbar>
);

const PostList = () => (
    <List actions={<PostListToolbar />}>
        <Datagrid>
            <TextField source="title" />
            <NumberField source="views" />
            <ReferenceArrayField tags="tags" source="tag_ids" />
            <BooleanField source="published" />
        </Datagrid>
    </List>
);
import {
    BooleanField,
    CreateButton,
    Datagrid,
    List,
    NumberField,
    ReferenceArrayField,
    TopToolbar,
    TextField,
} from 'react-admin';
import {
    StackedFilters,
    FiltersConfig,
    textFilter,
    numberFilter,
    referenceFilter,
    booleanFilter,
} from '@react-admin/ra-form-layout';

const postListFilters: FiltersConfig = {
    title: textFilter(),
    views: numberFilter(),
    tag_ids: referenceFilter({ reference: 'tags' }),
    published: booleanFilter(),
};

const PostListToolbar = () => (
    <TopToolbar>
        <CreateButton />
        <StackedFilters config={postListFilters} />
    </TopToolbar>
);

const PostList = () => (
    <List actions={<PostListToolbar />}>
        <Datagrid>
            <TextField source="title" />
            <NumberField source="views" />
            <ReferenceArrayField tags="tags" source="tag_ids" />
            <BooleanField source="published" />
        </Datagrid>
    </List>
);

Props

Prop Required Type Default Description
BadgeProps Optional object - Additional props to pass to the MUI Badge
ButtonProps Optional object - Additional props to pass to the Button
className Optional string - Additional CSS class applied on the root component
config Required (*) object - The stacked filters configuration
PopoverProps Optional Object - Additional props to pass to the MUI Popover
StackedFiltersFormProps Optional Object - Additional props to pass to the StackedFiltersForm
sx Optional Object - An object containing the MUI style overrides to apply to the root component

BadgeProps

This prop lets you pass additional props for the MUI Badge.

import {
    StackedFilters,
    StackedFiltersProps,
} from '@react-admin/ra-form-layout';

export const MyStackedFilter = (props: StackedFiltersProps) => (
    <StackedFilters {...props} BadgeProps={{ showZero: true }} />
);
import {
    StackedFilters,
    StackedFiltersProps,
} from '@react-admin/ra-form-layout';

export const MyStackedFilter = (props: StackedFiltersProps) => (
    <StackedFilters {...props} BadgeProps={{ showZero: true }} />
);

ButtonProps

This prop lets you pass additional props for the Button.

import {
    StackedFilters,
    StackedFiltersProps,
} from '@react-admin/ra-form-layout';

export const MyStackedFilter = (props: StackedFiltersProps) => (
    <StackedFilters {...props} ButtonProps={{ variant: 'contained' }} />
);
import {
    StackedFilters,
    StackedFiltersProps,
} from '@react-admin/ra-form-layout';

export const MyStackedFilter = (props: StackedFiltersProps) => (
    <StackedFilters {...props} ButtonProps={{ variant: 'contained' }} />
);

className

This prop lets you pass additional CSS classes to apply to the root element (a div).

import {
    StackedFilters,
    StackedFiltersProps,
} from '@react-admin/ra-form-layout';

export const MyStackedFilter = (props: StackedFiltersProps) => (
    <StackedFilters {...props} className="my-css-class" />
);
import {
    StackedFilters,
    StackedFiltersProps,
} from '@react-admin/ra-form-layout';

export const MyStackedFilter = (props: StackedFiltersProps) => (
    <StackedFilters {...props} className="my-css-class" />
);

config

This prop lets you define the filter configuration, which is required. This is an object defining the operators and UI for each source that can be used as a filter:

import { FiltersConfig, StackedFilters } from '@react-admin/ra-form-layout';
import { NumberInput } from 'react-admin';
import { MyNumberRangeInput } from './MyNumberRangeInput';

const postListFilters: FiltersConfig = {
    views: {
        operators: [
            { value: 'eq', label: 'Equals' },
            { value: 'neq', label: 'Not Equals' },
            {
                value: 'between',
                label: 'Between',
                input: ({ source }) => <MyNumberRangeInput source={source} />,
            },
        ],
        input: ({ source }) => <NumberInput source={source} />,
    },
};

export const MyStackedFilter = (props: StackedFiltersProps) => (
    <StackedFilters {...props} config={postListFilters} />
);
import { FiltersConfig, StackedFilters } from '@react-admin/ra-form-layout';
import { NumberInput } from 'react-admin';
import { MyNumberRangeInput } from './MyNumberRangeInput';

const postListFilters: FiltersConfig = {
    views: {
        operators: [
            { value: 'eq', label: 'Equals' },
            { value: 'neq', label: 'Not Equals' },
            {
                value: 'between',
                label: 'Between',
                input: ({ source }) => <MyNumberRangeInput source={source} />,
            },
        ],
        input: ({ source }) => <NumberInput source={source} />,
    },
};

export const MyStackedFilter = (props: StackedFiltersProps) => (
    <StackedFilters {...props} config={postListFilters} />
);

PopoverProps

This prop lets you pass additional props for the MUI Popover.

import {
    StackedFilters,
    StackedFiltersProps,
} from '@react-admin/ra-form-layout';

export const MyStackedFilter = (props: StackedFiltersProps) => (
    <StackedFilters {...props} PopoverProps={{ elevation: 4 }} />
);
import {
    StackedFilters,
    StackedFiltersProps,
} from '@react-admin/ra-form-layout';

export const MyStackedFilter = (props: StackedFiltersProps) => (
    <StackedFilters {...props} PopoverProps={{ elevation: 4 }} />
);

StackedFiltersFormProps

This prop lets you pass additional props for the StackedFiltersForm.

import {
    StackedFilters,
    StackedFiltersProps,
} from '@react-admin/ra-form-layout';

export const MyStackedFilter = (props: StackedFiltersProps) => (
    <StackedFilters
        {...props}
        StackedFiltersForm={{ className: 'my-css-class' }}
    />
);
import {
    StackedFilters,
    StackedFiltersProps,
} from '@react-admin/ra-form-layout';

export const MyStackedFilter = (props: StackedFiltersProps) => (
    <StackedFilters
        {...props}
        StackedFiltersForm={{ className: 'my-css-class' }}
    />
);

sx: CSS API

This prop lets you override the styles of the inner components thanks to the sx property. This property accepts the following subclasses:

Rule name Description
RaStackedFilters Applied to the root component
& .RaStackedFilters-popover Applied to the MUI Popover
& .RaStackedFilters-formContainer Applied to the form container (a div)

<StackedFiltersForm>

This component is responsible for handling the filtering form. It must be given the filtering configuration through its config prop.

If you need to be notified when users have applied filters, pass a function to the onFiltersApplied prop. This is useful if you want to close the filters container (<Modal>, <Drawer>, etc.).

import {
    Datagrid,
    List,
    TextField,
    NumberField,
    BooleanField,
    ReferenceArrayField,
} from 'react-admin';
import {
    StackedFiltersForm,
    FiltersConfig,
    textFilter,
    numberFilter,
    referenceFilter,
    booleanFilter,
} from '@react-admin/ra-form-layout';
import {
    Accordion,
    AccordionDetails,
    AccordionSummary,
    Card,
    Typography,
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';

const postListFilters: FiltersConfig = {
    title: textFilter(),
    views: numberFilter(),
    tag_ids: referenceFilter({ reference: 'tags' }),
    published: booleanFilter(),
};

const PostList = () => (
    <ListBase>
        <Accordion sx={{ my: 1 }}>
            <AccordionSummary
                expandIcon={<ExpandMoreIcon />}
                aria-controls="filters-content"
                id="filters-header"
            >
                <Typography>Filters</Typography>
            </AccordionSummary>
            <AccordionDetails id="filters-content">
                <StackedFiltersForm config={postListFilters} />
            </AccordionDetails>
        </Accordion>
        <Card>
            <Datagrid>
                <TextField source="title" />
                <NumberField source="views" />
                <ReferenceArrayField tags="tags" source="tag_ids" />
                <BooleanField source="published" />
            </Datagrid>
        </Card>
    </ListBase>
);
import {
    Datagrid,
    List,
    TextField,
    NumberField,
    BooleanField,
    ReferenceArrayField,
} from 'react-admin';
import {
    StackedFiltersForm,
    FiltersConfig,
    textFilter,
    numberFilter,
    referenceFilter,
    booleanFilter,
} from '@react-admin/ra-form-layout';
import {
    Accordion,
    AccordionDetails,
    AccordionSummary,
    Card,
    Typography,
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';

const postListFilters: FiltersConfig = {
    title: textFilter(),
    views: numberFilter(),
    tag_ids: referenceFilter({ reference: 'tags' }),
    published: booleanFilter(),
};

const PostList = () => (
    <ListBase>
        <Accordion sx={{ my: 1 }}>
            <AccordionSummary
                expandIcon={<ExpandMoreIcon />}
                aria-controls="filters-content"
                id="filters-header"
            >
                <Typography>Filters</Typography>
            </AccordionSummary>
            <AccordionDetails id="filters-content">
                <StackedFiltersForm config={postListFilters} />
            </AccordionDetails>
        </Accordion>
        <Card>
            <Datagrid>
                <TextField source="title" />
                <NumberField source="views" />
                <ReferenceArrayField tags="tags" source="tag_ids" />
                <BooleanField source="published" />
            </Datagrid>
        </Card>
    </ListBase>
);

Props

Prop Required Type Default Description
className Optional string - Additional CSS class applied on the root component
config Required (*) object - The stacked filters configuration
onFiltersApplied Optional Function - A function called when users click on the apply button
sx Optional Object - An object containing the MUI style overrides to apply to the root component

className

This prop lets you pass additional CSS classes to apply to the root element (a Form).

import {
    StackedFiltersForm,
    StackedFiltersFormProps,
} from '@react-admin/ra-form-layout';

export const MyStackedFilterForm = (props: StackedFiltersFormProps) => (
    <StackedFiltersForm {...props} className="my-css-class" />
);
import {
    StackedFiltersForm,
    StackedFiltersFormProps,
} from '@react-admin/ra-form-layout';

export const MyStackedFilterForm = (props: StackedFiltersFormProps) => (
    <StackedFiltersForm {...props} className="my-css-class" />
);

config

This prop lets you define the filter configuration, which is required. This is an object defining the operators and UI for each source that can be used as a filter:

import {
    FiltersConfig,
    StackedFiltersForm,
    StackedFiltersFormProps,
} from '@react-admin/ra-form-layout';
import { NumberInput } from 'react-admin';
import { MyNumberRangeInput } from './MyNumberRangeInput';

const postListFilters: FiltersConfig = {
    views: {
        operators: [
            { value: 'eq', label: 'Equals' },
            { value: 'neq', label: 'Not Equals' },
            {
                value: 'between',
                label: 'Between',
                input: ({ source }) => <MyNumberRangeInput source={source} />,
            },
        ],
        input: ({ source }) => <NumberInput source={source} />,
    },
};

export const MyStackedFiltersForm = (props: StackedFiltersFormProps) => (
    <StackedFiltersForm {...props} config={postListFilters} />
);
import {
    FiltersConfig,
    StackedFiltersForm,
    StackedFiltersFormProps,
} from '@react-admin/ra-form-layout';
import { NumberInput } from 'react-admin';
import { MyNumberRangeInput } from './MyNumberRangeInput';

const postListFilters: FiltersConfig = {
    views: {
        operators: [
            { value: 'eq', label: 'Equals' },
            { value: 'neq', label: 'Not Equals' },
            {
                value: 'between',
                label: 'Between',
                input: ({ source }) => <MyNumberRangeInput source={source} />,
            },
        ],
        input: ({ source }) => <NumberInput source={source} />,
    },
};

export const MyStackedFiltersForm = (props: StackedFiltersFormProps) => (
    <StackedFiltersForm {...props} config={postListFilters} />
);

onFiltersApplied

This prop lets you provide a function that will be called when users click the apply button:

import { FiltersConfig, StackedFiltersForm } from '@react-admin/ra-form-layout';

export const MyStackedFiltersForm = (props: StackedFiltersProps) => (
    <StackedFiltersForm
        {...props}
        onFiltersApplied={() => alert('Filters applied')}
    />
);
import { FiltersConfig, StackedFiltersForm } from '@react-admin/ra-form-layout';

export const MyStackedFiltersForm = (props: StackedFiltersProps) => (
    <StackedFiltersForm
        {...props}
        onFiltersApplied={() => alert('Filters applied')}
    />
);

sx: CSS API

This prop lets you override the styles of the inner components thanks to the sx property. This property accepts the following subclasses:

Rule name Description
RaStackedFiltersForm Applied to the root component
& .RaStackedFiltersForm-sourceInput Applied to the AutocompleteInput that allows users to select the field
& .RaStackedFiltersForm-operatorInput Applied to the SelectInput that allows users to select the field
& .RaStackedFiltersForm-valueInput Applied to the input that allows users to set the filter value

<AutoSave>

A component that enables autosaving of the form. It's ideal for long data entry tasks, and reduces the risk of data loss.

Usage

Put <AutoSave> inside a react-admin form (<SimpleForm>, <TabbedForm>, <LongForm>, etc.), for instance in a custom toolbar.

Note that you must set the <Form resetOptions> prop to { keepDirtyValues: true }. If you forget that prop, any change entered by the end user after the autosave but before its acknowledgement by the server will be lost.

If you're using it in an <Edit> page, you must also use a pessimistic or optimistic mutationMode - <AutoSave> doesn't work with the default mutationMode="undoable".

import { AutoSave } from '@react-admin/ra-form-layout';
import { Edit, SaveButton, SimpleForm, TextInput, Toolbar } from 'react-admin';

const AutoSaveToolbar = () => (
    <Toolbar>
        <SaveButton />
        <AutoSave />
    </Toolbar>
);

const PostEdit = () => (
    <Edit mutationMode="optimistic">
        <SimpleForm
            resetOptions={{ keepDirtyValues: true }}
            toolbar={<AutoSaveToolbar />}
        >
            <TextInput source="title" />
            <TextInput source="teaser" />
        </SimpleForm>
    </Edit>
);
import { AutoSave } from '@react-admin/ra-form-layout';
import { Edit, SaveButton, SimpleForm, TextInput, Toolbar } from 'react-admin';

const AutoSaveToolbar = () => (
    <Toolbar>
        <SaveButton />
        <AutoSave />
    </Toolbar>
);

const PostEdit = () => (
    <Edit mutationMode="optimistic">
        <SimpleForm
            resetOptions={{ keepDirtyValues: true }}
            toolbar={<AutoSaveToolbar />}
        >
            <TextInput source="title" />
            <TextInput source="teaser" />
        </SimpleForm>
    </Edit>
);

The app will save the current form values after 3 seconds of inactivity.

Props

  • debounce: The interval in milliseconds between two autosaves. Defaults to 3000 (3s).
  • confirmationDuration: The delay in milliseconds before save confirmation message disappears. Defaults to 3000 (3s). When set to false, the confirmation message will not disappear.
  • typographyProps: Additional props to pass to the <Typography> component that displays the confirmation and error messages.

useAutoSave

A hook that automatically saves the form at a regular interval. It works for the pessimistic and optimistic mutationMode but not for the undoable.

It accepts the following parameters:

  • debounce: The interval in ms between two saves. Defaults to 3000 (3s).
  • onSuccess: A callback to call when the save request succeeds.
  • onError: A callback to call when the save request fails.
  • transform: A function to transform the data before saving.

Note that you must add the resetOptions prop with { keepDirtyValues: true } to avoid having the user changes overridden by the latest update operation result.

import { useAutoSave } from '@react-admin/ra-form-layout';
import { Edit, SaveButton, SimpleForm, TextInput, Toolbar } from 'react-admin';

const AutoSave = () => {
    const [lastSave, setLastSave] = useState();
    const [error, setError] = useState();
    useAutoSave({
        interval: 5000,
        onSuccess: () => setLastSave(new Date()),
        onError: error => setError(error),
    });
    return (
        <div>
            {lastSave && <p>Saved at {lastSave.toLocaleString()}</p>}
            {error && <p>Error: {error}</p>}
        </div>
    );
};

const AutoSaveToolbar = () => (
    <Toolbar>
        <SaveButton />
        <AutoSave />
    </Toolbar>
);

const PostEdit = () => (
    <Edit mutationMode="optimistic">
        <SimpleForm
            resetOptions={{ keepDirtyValues: true }}
            toolbar={<AutoSaveToolbar />}
        >
            <TextInput source="title" />
            <TextInput source="teaser" />
        </SimpleForm>
    </Edit>
);
import { useAutoSave } from '@react-admin/ra-form-layout';
import { Edit, SaveButton, SimpleForm, TextInput, Toolbar } from 'react-admin';

const AutoSave = () => {
    const [lastSave, setLastSave] = useState();
    const [error, setError] = useState();
    useAutoSave({
        interval: 5000,
        onSuccess: () => setLastSave(new Date()),
        onError: error => setError(error),
    });
    return (
        <div>
            {lastSave && <p>Saved at {lastSave.toLocaleString()}</p>}
            {error && <p>Error: {error}</p>}
        </div>
    );
};

const AutoSaveToolbar = () => (
    <Toolbar>
        <SaveButton />
        <AutoSave />
    </Toolbar>
);

const PostEdit = () => (
    <Edit mutationMode="optimistic">
        <SimpleForm
            resetOptions={{ keepDirtyValues: true }}
            toolbar={<AutoSaveToolbar />}
        >
            <TextInput source="title" />
            <TextInput source="teaser" />
        </SimpleForm>
    </Edit>
);

useAutoSave returns a boolean indicating whether the form is currently being saved.

const isSaving = useAutoSave({
    interval: 5000,
    onSuccess: () => setLastSave(new Date()),
    onError: error => setError(error),
});

<BulkUpdateFormButton>

This component renders a button allowing to edit multiple records at once.

The button opens a dialog containing the form passed as children. When the form is submitted, it will call the dataProvider's updateMany method with the ids of the selected records.

Usage

<BulkUpdateFormButton> can be used inside <Datagrid>'s bulkActionButtons.

import * as React from 'react';
import {
    Admin,
    BooleanField,
    BooleanInput,
    Datagrid,
    DateField,
    DateInput,
    List,
    Resource,
    SimpleForm,
    TextField,
} from 'react-admin';
import { BulkUpdateFormButton } from '@react-admin/ra-form-layout';

import { dataProvider } from './dataProvider';
import { i18nProvider } from './i18nProvider';

export const App = () => (
    <Admin dataProvider={dataProvider} i18nProvider={i18nProvider}>
        <Resource name="posts" list={PostList} />
    </Admin>
);

const PostBulkUpdateButton = () => (
    <BulkUpdateFormButton>
        <SimpleForm>
            <DateInput source="published_at" />
            <BooleanInput source="is_public" />
        </SimpleForm>
    </BulkUpdateFormButton>
);

const PostList = () => (
    <List>
        <Datagrid bulkActionButtons={<PostBulkUpdateButton />}>
            <TextField source="id" />
            <TextField source="title" />
            <DateField source="published_at" />
            <BooleanField source="is_public" />
        </Datagrid>
    </List>
);
import * as React from 'react';
import {
    Admin,
    BooleanField,
    BooleanInput,
    Datagrid,
    DateField,
    DateInput,
    List,
    Resource,
    SimpleForm,
    TextField,
} from 'react-admin';
import { BulkUpdateFormButton } from '@react-admin/ra-form-layout';

import { dataProvider } from './dataProvider';
import { i18nProvider } from './i18nProvider';

export const App = () => (
    <Admin dataProvider={dataProvider} i18nProvider={i18nProvider}>
        <Resource name="posts" list={PostList} />
    </Admin>
);

const PostBulkUpdateButton = () => (
    <BulkUpdateFormButton>
        <SimpleForm>
            <DateInput source="published_at" />
            <BooleanInput source="is_public" />
        </SimpleForm>
    </BulkUpdateFormButton>
);

const PostList = () => (
    <List>
        <Datagrid bulkActionButtons={<PostBulkUpdateButton />}>
            <TextField source="id" />
            <TextField source="title" />
            <DateField source="published_at" />
            <BooleanField source="is_public" />
        </Datagrid>
    </List>
);

Tip: You are not limited to using a <SimpleForm> as children. You can for instance use an <InputSelectorForm>, which allows to select the fields to update.

import {
    BulkUpdateFormButton,
    InputSelectorForm,
} from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput } from 'react-admin';

const PostBulkUpdateButton = () => (
    <BulkUpdateFormButton>
        <InputSelectorForm
            inputs={[
                {
                    label: 'Published at',
                    element: <DateInput source="published_at" />,
                },
                {
                    label: 'Is public',
                    element: <BooleanInput source="is_public" />,
                },
            ]}
        />
    </BulkUpdateFormButton>
);
import {
    BulkUpdateFormButton,
    InputSelectorForm,
} from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput } from 'react-admin';

const PostBulkUpdateButton = () => (
    <BulkUpdateFormButton>
        <InputSelectorForm
            inputs={[
                {
                    label: 'Published at',
                    element: <DateInput source="published_at" />,
                },
                {
                    label: 'Is public',
                    element: <BooleanInput source="is_public" />,
                },
            ]}
        />
    </BulkUpdateFormButton>
);

Check out the <InputSelectorForm> documentation for more information.

Props

Prop Required Type Default Description
children Required (*) Element - A form component to render inside the Dialog
DialogProps - Object - Additional props to pass to the MUI Dialog
mutationMode - string 'pessimistic' The mutation mode ('undoable', 'pessimistic' or 'optimistic')
mutationOptions - Object - Mutation options passed to react-query when calling updateMany

children

<BulkUpdateFormButton> expects a form component as children, such as <SimpleForm> or <InputSelectorForm>.

import { BulkUpdateFormButton } from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput, SimpleForm } from 'react-admin';

const PostBulkUpdateButton = () => (
    <BulkUpdateFormButton>
        <SimpleForm>
            <DateInput source="published_at" />
            <BooleanInput source="is_public" />
        </SimpleForm>
    </BulkUpdateFormButton>
);
import { BulkUpdateFormButton } from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput, SimpleForm } from 'react-admin';

const PostBulkUpdateButton = () => (
    <BulkUpdateFormButton>
        <SimpleForm>
            <DateInput source="published_at" />
            <BooleanInput source="is_public" />
        </SimpleForm>
    </BulkUpdateFormButton>
);

DialogProps

The DialogProps prop can be used to pass additional props to the MUI Dialog.

import { Slide } from '@mui/material';
import { TransitionProps } from '@mui/material/transitions';
import { BulkUpdateFormButton } from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput, SimpleForm } from 'react-admin';

const Transition = React.forwardRef(function Transition(
    props: TransitionProps & {
        children: React.ReactElement<any, any>;
    },
    ref: React.Ref<unknown>
) {
    return <Slide direction="left" ref={ref} {...props} />;
});

const PostBulkUpdateButtonWithTransition = () => (
    <BulkUpdateFormButton DialogProps={{ TransitionComponent: Transition }}>
        <SimpleForm>
            <DateInput source="published_at" />
            <BooleanInput source="is_public" />
        </SimpleForm>
    </BulkUpdateFormButton>
);
import { Slide } from '@mui/material';
import { TransitionProps } from '@mui/material/transitions';
import { BulkUpdateFormButton } from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput, SimpleForm } from 'react-admin';

const Transition = React.forwardRef(function Transition(
    props: TransitionProps & {
        children: React.ReactElement<any, any>;
    },
    ref: React.Ref<unknown>
) {
    return <Slide direction="left" ref={ref} {...props} />;
});

const PostBulkUpdateButtonWithTransition = () => (
    <BulkUpdateFormButton DialogProps={{ TransitionComponent: Transition }}>
        <SimpleForm>
            <DateInput source="published_at" />
            <BooleanInput source="is_public" />
        </SimpleForm>
    </BulkUpdateFormButton>
);

mutationMode

Use the mutationMode prop to specify the mutation mode.

import { BulkUpdateFormButton } from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput, SimpleForm } from 'react-admin';

const PostBulkUpdateButton = () => (
    <BulkUpdateFormButton mutationMode="undoable">
        <SimpleForm>
            <DateInput source="published_at" />
            <BooleanInput source="is_public" />
        </SimpleForm>
    </BulkUpdateFormButton>
);
import { BulkUpdateFormButton } from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput, SimpleForm } from 'react-admin';

const PostBulkUpdateButton = () => (
    <BulkUpdateFormButton mutationMode="undoable">
        <SimpleForm>
            <DateInput source="published_at" />
            <BooleanInput source="is_public" />
        </SimpleForm>
    </BulkUpdateFormButton>
);

mutationOptions and meta

The mutationOptions prop can be used to pass options to the react-query mutation used to call the dataProvider's updateMany method.

import { BulkUpdateFormButton } from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput, SimpleForm } from 'react-admin';

const PostBulkUpdateButton = () => (
    <BulkUpdateFormButton mutationOptions={{ retry: false }}>
        <SimpleForm>
            <DateInput source="published_at" />
            <BooleanInput source="is_public" />
        </SimpleForm>
    </BulkUpdateFormButton>
);
import { BulkUpdateFormButton } from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput, SimpleForm } from 'react-admin';

const PostBulkUpdateButton = () => (
    <BulkUpdateFormButton mutationOptions={{ retry: false }}>
        <SimpleForm>
            <DateInput source="published_at" />
            <BooleanInput source="is_public" />
        </SimpleForm>
    </BulkUpdateFormButton>
);

You can also use this prop to pass a meta object, that will be passed to the dataProvider when calling updateMany.

import { BulkUpdateFormButton } from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput, SimpleForm } from 'react-admin';

const PostBulkUpdateButton = () => (
    <BulkUpdateFormButton mutationOptions={{ meta: { foo: 'bar' } }}>
        <SimpleForm>
            <DateInput source="published_at" />
            <BooleanInput source="is_public" />
        </SimpleForm>
    </BulkUpdateFormButton>
);
import { BulkUpdateFormButton } from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput, SimpleForm } from 'react-admin';

const PostBulkUpdateButton = () => (
    <BulkUpdateFormButton mutationOptions={{ meta: { foo: 'bar' } }}>
        <SimpleForm>
            <DateInput source="published_at" />
            <BooleanInput source="is_public" />
        </SimpleForm>
    </BulkUpdateFormButton>
);

Usage with <TabbedForm> or other location based form layouts

<BulkUpdateFormButton> can be used with any form layout. However, for form layouts that are based on location by default, such as <TabbedForm>, you will need to disable the location syncing feature, as it may conflict with the Edit route declared by React Admin (/<resource>/<id>).

For instance, with <TabbedForm>, you can use the syncWithLocation prop to disable it:

import { BulkUpdateFormButton } from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput, TabbedForm } from 'react-admin';

const PostBulkUpdateButton = () => (
    <BulkUpdateFormButton>
        <TabbedForm syncWithLocation={false}>
            <TabbedForm.Tab label="Publication">
                <DateInput source="published_at" />
            </TabbedForm.Tab>
            <TabbedForm.Tab label="Visibility">
                <BooleanInput source="is_public" />
            </TabbedForm.Tab>
        </TabbedForm>
    </BulkUpdateFormButton>
);
import { BulkUpdateFormButton } from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput, TabbedForm } from 'react-admin';

const PostBulkUpdateButton = () => (
    <BulkUpdateFormButton>
        <TabbedForm syncWithLocation={false}>
            <TabbedForm.Tab label="Publication">
                <DateInput source="published_at" />
            </TabbedForm.Tab>
            <TabbedForm.Tab label="Visibility">
                <BooleanInput source="is_public" />
            </TabbedForm.Tab>
        </TabbedForm>
    </BulkUpdateFormButton>
);

Limitations

If you look under the hood, you will see that <BulkUpdateFormButton> provides a <SaveContext> to its children, which allows them to call updateMany with the ids of the selected records.

However since we are in the context of a list, there is no <RecordContext> available. Hence, the following inputs cannot work inside a <BulkUpdateFormButton>:

  • <ReferenceOneInput>
  • <ReferenceManyInput>
  • <ReferenceManyToManyInput>

Also, please note that it is not possible to use a transform function with <BulkUpdateFormButton>.

<InputSelectorForm>

This component renders a form allowing to select the fields to update in a record.

Usage

<InputSelectorForm> expects a list of inputs passed in the inputs prop. Each input must have a label and an element.

import { InputSelectorForm } from '@react-admin/ra-form-layout';
import * as React from 'react';
import {
    BooleanInput,
    DateInput,
    SelectArrayInput,
    TextInput,
} from 'react-admin';

const PostEdit = () => (
    <InputSelectorForm
        inputs={[
            {
                label: 'Title',
                element: <TextInput source="title" />,
            },
            {
                label: 'Body',
                element: <TextInput source="body" multiline />,
            },
            {
                label: 'Published at',
                element: <DateInput source="published_at" />,
            },
            {
                label: 'Is public',
                element: <BooleanInput source="is_public" />,
            },
            {
                label: 'Tags',
                element: (
                    <SelectArrayInput
                        source="tags"
                        choices={[
                            { id: 'react', name: 'React' },
                            { id: 'vue', name: 'Vue' },
                            { id: 'solid', name: 'Solid' },
                            { id: 'programming', name: 'Programming' },
                        ]}
                    />
                ),
            },
        ]}
    />
);
import { InputSelectorForm } from '@react-admin/ra-form-layout';
import * as React from 'react';
import {
    BooleanInput,
    DateInput,
    SelectArrayInput,
    TextInput,
} from 'react-admin';

const PostEdit = () => (
    <InputSelectorForm
        inputs={[
            {
                label: 'Title',
                element: <TextInput source="title" />,
            },
            {
                label: 'Body',
                element: <TextInput source="body" multiline />,
            },
            {
                label: 'Published at',
                element: <DateInput source="published_at" />,
            },
            {
                label: 'Is public',
                element: <BooleanInput source="is_public" />,
            },
            {
                label: 'Tags',
                element: (
                    <SelectArrayInput
                        source="tags"
                        choices={[
                            { id: 'react', name: 'React' },
                            { id: 'vue', name: 'Vue' },
                            { id: 'solid', name: 'Solid' },
                            { id: 'programming', name: 'Programming' },
                        ]}
                    />
                ),
            },
        ]}
    />
);

<InputSelectorForm> also expects to be used inside a <SaveContext>. When the form is submitted, it will call the save method from the <SaveContext>, with the value of the selected inputs.

Tip: <InputSelectorForm> is particularily useful when used with <BulkUpdateFormButton>, as it allows to select the fields to update.

import {
    BulkUpdateFormButton,
    InputSelectorForm,
} from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput } from 'react-admin';

const PostBulkUpdateButton = () => (
    <BulkUpdateFormButton>
        <InputSelectorForm
            inputs={[
                {
                    label: 'Published at',
                    element: <DateInput source="published_at" />,
                },
                {
                    label: 'Is public',
                    element: <BooleanInput source="is_public" />,
                },
            ]}
        />
    </BulkUpdateFormButton>
);
import {
    BulkUpdateFormButton,
    InputSelectorForm,
} from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput } from 'react-admin';

const PostBulkUpdateButton = () => (
    <BulkUpdateFormButton>
        <InputSelectorForm
            inputs={[
                {
                    label: 'Published at',
                    element: <DateInput source="published_at" />,
                },
                {
                    label: 'Is public',
                    element: <BooleanInput source="is_public" />,
                },
            ]}
        />
    </BulkUpdateFormButton>
);

Check out the <BulkUpdateFormButton> documentation for more information.

Props

Prop Required Type Default Description
inputs Required (*) Array - The list of inputs from which the user can pick

<InputSelectorForm> also accepts the same props as <WizardForm>, except the onSubmit and children props.

inputs

Use the inputs prop to specify the list of inputs from which the user can pick. Each input must have a label and an element.

import { InputSelectorForm } from '@react-admin/ra-form-layout';
import * as React from 'react';
import {
    BooleanInput,
    DateInput,
    SelectArrayInput,
    TextInput,
} from 'react-admin';

const PostEdit = () => (
    <InputSelectorForm
        inputs={[
            {
                label: 'Title',
                element: <TextInput source="title" />,
            },
            {
                label: 'Body',
                element: <TextInput source="body" multiline />,
            },
            {
                label: 'Published at',
                element: <DateInput source="published_at" />,
            },
            {
                label: 'Is public',
                element: <BooleanInput source="is_public" />,
            },
            {
                label: 'Tags',
                element: (
                    <SelectArrayInput
                        source="tags"
                        choices={[
                            { id: 'react', name: 'React' },
                            { id: 'vue', name: 'Vue' },
                            { id: 'solid', name: 'Solid' },
                            { id: 'programming', name: 'Programming' },
                        ]}
                    />
                ),
            },
        ]}
    />
);
import { InputSelectorForm } from '@react-admin/ra-form-layout';
import * as React from 'react';
import {
    BooleanInput,
    DateInput,
    SelectArrayInput,
    TextInput,
} from 'react-admin';

const PostEdit = () => (
    <InputSelectorForm
        inputs={[
            {
                label: 'Title',
                element: <TextInput source="title" />,
            },
            {
                label: 'Body',
                element: <TextInput source="body" multiline />,
            },
            {
                label: 'Published at',
                element: <DateInput source="published_at" />,
            },
            {
                label: 'Is public',
                element: <BooleanInput source="is_public" />,
            },
            {
                label: 'Tags',
                element: (
                    <SelectArrayInput
                        source="tags"
                        choices={[
                            { id: 'react', name: 'React' },
                            { id: 'vue', name: 'Vue' },
                            { id: 'solid', name: 'Solid' },
                            { id: 'programming', name: 'Programming' },
                        ]}
                    />
                ),
            },
        ]}
    />
);

Internationalization

You can use translation keys as input labels.

// in i18n/fr.ts

const customFrenchMessages = {
    resources: {
        posts: {
            name: 'Article |||| Articles',
            fields: {
                published_at: 'Publié le',
                is_public: 'Public',
            },
        },
    },
};

// in posts/postEdit.tsx

import { InputSelectorForm } from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput } from 'react-admin';

const PostEdit = () => (
    <InputSelectorForm
        inputs={[
            {
                label: 'resources.posts.fields.published_at',
                element: <DateInput source="published_at" />,
            },
            {
                label: 'resources.posts.fields.is_public',
                element: <BooleanInput source="is_public" />,
            },
        ]}
    />
);
// in i18n/fr.ts

const customFrenchMessages = {
    resources: {
        posts: {
            name: 'Article |||| Articles',
            fields: {
                published_at: 'Publié le',
                is_public: 'Public',
            },
        },
    },
};

// in posts/postEdit.tsx

import { InputSelectorForm } from '@react-admin/ra-form-layout';
import * as React from 'react';
import { BooleanInput, DateInput } from 'react-admin';

const PostEdit = () => (
    <InputSelectorForm
        inputs={[
            {
                label: 'resources.posts.fields.published_at',
                element: <DateInput source="published_at" />,
            },
            {
                label: 'resources.posts.fields.is_public',
                element: <BooleanInput source="is_public" />,
            },
        ]}
    />
);

<DateInput>, <DateTimeInput> and <TimeInput>

<DateInput>, <DateTimeInput> and <TimeInput> are wrappers around the MUI X Date/Time pickers. They allow for more customization of the UI than the default browser pickers. They also make it easier to work with specific locale and date formats.

Usage

Use <DateInput>, <DateTimeInput> or <TimeInput> inside a form component (<SimpleForm>, <TabbedForm>, <LongForm>, etc.) to allow users to pick a date, a time or both.

import {
    DateInput,
    DateTimeInput,
    TimeInput,
} from '@react-admin/ra-form-layout';
import { Edit, SimpleForm } from 'react-admin';

export const EventEdit = () => (
    <Edit>
        <SimpleForm>
            <DateInput source="event_date" />
            <TimeInput source="event_start_time" />
            <DateTimeInput source="published_at" />
        </SimpleForm>
    </Edit>
);
import {
    DateInput,
    DateTimeInput,
    TimeInput,
} from '@react-admin/ra-form-layout';
import { Edit, SimpleForm } from 'react-admin';

export const EventEdit = () => (
    <Edit>
        <SimpleForm>
            <DateInput source="event_date" />
            <TimeInput source="event_start_time" />
            <DateTimeInput source="published_at" />
        </SimpleForm>
    </Edit>
);

<DateInput>, <DateTimeInput> or <TimeInput> will accept either a Date object or any string that can be parsed into a Date as value. It will return a Date object, or null if the date is invalid.

Tip: You can use the parse prop to change the format of the returned value. See Parsing the date/time as an ISO string for an example.

Props

Prop Required Type Default Description
fullWidth - boolean false If true, the input will expand to fill the form width
helperText - string - Text to be displayed under the input
mask - string - Alias for the MUI format prop. Format of the date/time when rendered in the input. Defaults to localized format.
parse - Function value => value === '' ? null : value Callback taking the input value, and returning the value you want stored in the form state.
validate - Function or Array - Validation rules for the input. See the Validation Documentation for details.

Except for the format prop (renamed mask), <DateInput>, <DateTimeInput> and <TimeInput> accept the same props as the MUI X Date/Time pickers. They also accept the common input props.

Providing your own LocalizationProvider

MUI X Pickers need to be wrapped in a LocalizationProvider to work properly. <DateInput>, <DateTimeInput> and <TimeInput> already include a default <LocalizationProvider> using the date-fns adapter and the enUS locale.

You can change the locale and the date format globally by wrapping the <Admin> with your own <LocalizationProvider>.

Here is how to set up the pickers to use the fr locale:

import { Admin, Resource } from 'react-admin';
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { fr } from 'date-fns/locale';
import { EventEdit } from './events';

export const App = () => (
    <LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={fr}>
        <Admin>
            <Resource name="events" edit={EventEdit} />
        </Admin>
    </LocalizationProvider>
);
import { Admin, Resource } from 'react-admin';
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { fr } from 'date-fns/locale';
import { EventEdit } from './events';

export const App = () => (
    <LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={fr}>
        <Admin>
            <Resource name="events" edit={EventEdit} />
        </Admin>
    </LocalizationProvider>
);

Note: React Admin only supports the date-fns adapter for now.

Parsing the date/time as an ISO string

By default, <DateInput>, <DateTimeInput> and <TimeInput> store the date/time as a Date object in the form state. If you wish to store the date/time as an ISO string instead (or any other format), you can use the parse prop.

<DateInput
    source="published"
    parse={(date: Date) => (date ? date.toISOString() : null)}
/>
<DateInput
    source="published"
    parse={(date: Date) => (date ? date.toISOString() : null)}
/>

CHANGELOG

v4.12.2

2024-02-27

  • Fix badly positioned label in <WizardForm> outlined <TextField>

v4.12.1

2024-02-26

  • Fix the <LongForm> RaLongForm root class is not exposed

v4.12.0

2024-02-08

  • Accept a React element for <AccordionFormPanel label>, <AccordionFormPanel secondary>, <AccordionSection label>, <AccordionSection secondary>
  • Accept an id prop for AccordionFormPanel and AccordionSection components

v4.11.2

2024-02-07

  • Add support for <AccordionFrom sx> and <AccordionFromPanel sx> props.

v4.11.1

2024-01-29

  • Fix EditDialog passes the queryOptions prop to the MaterialUI Dialog component.

v4.11.0

2024-01-15

  • Introduce <DateInput>, <DateTimeInput> and <TimeInput>, date/time picker components based on MUI X.

v4.10.4

2023-12-12

  • Add <AccordionFormPanel count> and <AccordionSection count> to allow customizing the count displayed in the accordion summary

v4.10.3

2023-12-01

  • Fix <BulkUpdateFormButton> does not disable the save button while saving

v4.10.2

2023-10-27

  • Fix lodash import to avoid bundling the entire library

v4.10.1

2023-10-09

  • Update documentation to explain how to use <StackedFilters> well.

v4.10.0

2023-10-05

  • Add support for className and sx props to <StackedFilters>, <StackedFiltersForm> and <StackedFiltersActions> components.

v4.9.7

2023-10-03

  • Fix <InputSelectorForm> briefly displays a validation error notification after submitting the form with React 18

v4.9.6

2023-10-03

  • Fix <InputSelectorForm> briefly displays a validation error after submitting the form
  • Fix <BulkUpdateFormButton> does not close the dialog when providing a custom onSuccess callback

v4.9.5

2023-10-02

  • Fix redirection in custom onSuccess is ignored by <CreateDialog> and <EditDialog>

v4.9.4

2023-09-05

  • Fix passing custom onSuccess does not close the dialog

v4.9.3

2023-08-30

  • Fix Dialogs don't pass MUI onClose arguments to their close handler

v4.9.2

2023-08-10

  • Fix <EditInDialogButton> causes console warning when using the transform prop

v4.9.1

2023-08-03

  • Fix <WizardForm/> makes it hard to override the progress margins.

v4.9.0

2023-07-06

  • Introduce <BulkUpdateButton>
  • Introduce <InputSelectorForm>

v4.8.3

2023-06-19

  • Fix <WizardForm> to support middlewares.

v4.8.2

2023-06-16

  • Fix exports that are problematic with some bundlers

v4.8.1

2023-06-14

  • Fix <StackedFilters> uses an incorrect translation key for the Filters button label (ra-form-layout.stacked_filters.filters_button_label is now ra-form-layout.filters.filters_button_label)
  • Fix labels in the <StackedFiltersForm> now have a hard-coded label supporting translations:
    • ra-form-layout.filters.source
    • ra-form-layout.filters.operator
    • ra-form-layout.filters.value

v4.8.0

2023-05-05

  • Add <AutoSave> component to automatically save a form when the user stops typing.
  • Add useAutoSave hook
  • Add <PreviousButton> for customizing the <WizardForm> toolbar.
  • Add <WizardForm.Step> shortcut to <WizardFormStep>
  • Add <AccordionForm.Panel> shortcut to <AccordionFormPanel>

This version requires react-admin version 4.11.0 or higher.

v4.7.1

2023-05-30

  • Fix compatibility with latest react-hook-form versions (>= 7.43), and hence with react-admin >= v4.11

v4.7.0

2023-05-24

  • Upgraded to react-admin 4.10.6

v4.6.2

2023-03-17

  • Fix usage of cloneElement by ensuring children are React elements.

v4.6.1

2023-03-02

  • Fix <EditDialog> ignores redirect prop when passing custom mutationOptions
  • Fix MUI warning when passing mutationMode to <EditDialog>

v4.6.0

2023-02-01

  • Added the <StackedFilters> component.

v4.5.3

2023-01-25

  • Fix React warnings about unknown or invalid props

v4.5.2

2022-10-28

  • (fix) Fix WizardForm next button disabled status
  • (doc) Fix WizardForm custom toolbar example

v4.5.1

2022-10-24

  • (fix) Add missing exports for CreateInDialogButton, EditInDialogButton and ShowInDialogButton

v4.5.0

2022-10-12

  • Feat: Add ability to use CreateDialog, EditDialog and ShowDialog standalone, without routing

v4.4.0

2022-08-29

  • Feat: Provide record in context for their title to EditDialog & ShowDialog

v4.3.0

2022-08-25

  • Remove <JsonSchemaForm> component. (new location in ra-json-schema-form)

v4.2.0

2022-07-29

  • Add <JsonSchemaForm> component.

v4.1.5

2022-07-21

  • Fix redirect prop is ignored by <CreateDialog> and <EditDialog>

v4.1.4

2022-07-01

  • Fix <AccordionSection> style (summary height, bottom border, etc.)

v4.1.3

2022-06-29

  • Fix: Replace classnames with clsx

v4.1.2

2022-06-21

  • Fix <EditDialog> not calling dataProvider.update when mutationMode is undefined
  • Fix Dialog Forms not working properly with <TabbedForm>
  • Doc: Add hasCreate in the Dialog Forms examples

v4.1.1

2022-06-20

  • Fix Dialog Forms are not displayed when <Admin> has its basename prop set.

v4.1.0

2022-06-16

  • Add <LongForm> component

v4.0.3

2022-06-10

  • (fix) Fix <EditDialog> and <CreateDialog> scroll to top on submit and on cancel

v4.0.2

2022-06-10

  • (fix) Fix <WizardForm> does not trigger save action

v4.0.1

2022-06-08

  • (fix) Update peer dependencies ranges (support React 18)

v4.0.0

2022-06-07

  • Upgrade to react-admin v4

v1.9.0

2022-01-05

  • (feat) Add <ShowDialog> component

v1.8.1

2021-12-17

  • (fix) Fix sanitize mutationMode out of WizardFormView
  • (fix) Fix change justify for justifyContent prop

v1.8.0

2021-11-12

  • (feat) Add ability to pass custom <Stepper> props to <WizardProgress>

v1.7.0

2021-08-03

  • (feat) Add translation key support for the label prop of the <WizardFormStep>

v1.6.2

2021-07-06

  • (doc) Add an example of summary step for the <WizardForm>

v1.6.1

2021-06-29

  • (fix) Update peer dependencies ranges (support react 17)

v1.6.0

2021-05-17

  • (chore) Update AccordionForm to use FormGroupContext for error tracking.
  • (feat) Ensure AccordionFormPanel, AccordionFormToolbar and FormDialogTitle styles are overridable through Material UI theme by providing it a key (RaAccordionFormPanel, RaAccordionFormToolbar and RaFormDialogTitle).

v1.5.5

2021-04-29

  • (fix) Allow additional properties on AccordionSection component

v1.5.4

2021-01-29

  • (fix) Fix wizard form does not handle submit on enter correctly

v1.5.3

2021-01-18

  • (fix) Fix dialog forms

v1.5.2

2020-11-04

  • (fix) Fix dialog forms prop interfaces

v1.5.1

2020-11-03

  • (fix) Fix providing sub-components (Accordion, <AccordionSummary> and <AccordionDetails>) should not be required.

v1.5.0

2020-11-02

  • (feat) Allow customizing the accordion sub-components (Accordion, <AccordionSummary> and <AccordionDetails>) by providing your own.

v1.4.0

2020-10-26

  • (feat) Allow customizing the accordion sub-components (Accordion, <AccordionSummary> and <AccordionDetails>)
  • (feat) Add types for the <AccordionSection>

v1.3.0

2020-10-05

  • (deps) Upgrade react-admin to v3.9.0

v1.2.0

2020-10-01

  • (feat) Dialog Form (CreateDialog & EditDialog)

v1.1.0

2020-09-28

  • (feat) Wizard Form

v1.0.1

2020-09-22

  • (fix) Fix Storybook error on history.replace

v1.0.0

2020-09-22

  • First release