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

ra-accordion-form

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.

AccordionForm

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.

<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 - The main label used as the accordion summary. Appears in red when the accordion has errors
children Required ReactNode - A list of <Input> elements
secondary Optional string - 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.
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 } 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>
);

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

AccordionSection

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.
label Required string - The main label used as the accordion summary.
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 - 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";

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.

WizardForm

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,
    WizardFormStep,
    WizardFormProgressProps,
} from '@react-admin/ra-form-layout';

const MyProgress = ({
    currentStep,
    onStepClick,
    steps,
}: WizardFormProgressProps) => (
    <ul>
        {steps.map((step, index) => {
            const label = React.cloneElement(step, { intent: 'label' });

            return (
                <li key={`step_${index}`}>
                    {!onStepClick ? (
                        <span
                            className={
                                currentStep === index ? 'active' : undefined
                            }
                        >
                            {label}
                        </span>
                    ) : (
                        <button onClick={() => onStepClick(index)}>
                            {label}
                        </button>
                    )}
                </li>
            );
        })}
    </ul>
);

const PostCreate = () => (
    <Create>
        <WizardForm progress={MyProgress}>
            <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 MyProgress = ({ currentStep, onStepClick, steps }) => (
    <ul>
        {steps.map((step, index) => {
            const label = React.cloneElement(step, { intent: "label" });

            return (
                <li key={`step_${index}`}>
                    {!onStepClick ? (
                        <span className={currentStep === index ? "active" : undefined}>{label}</span>
                    ) : (
                        <button onClick={() => onStepClick(index)}>{label}</button>
                    )}
                </li>
            );
        })}
    </ul>
);

const PostCreate = () => (
    <Create>
        <WizardForm progress={MyProgress}>
            <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>
);

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

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.

LongForm

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

EditDialog

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.

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

InDialogButtons

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 from "react";
import {
    Datagrid,
    DateField,
    DateInput,
    Edit,
    ReferenceManyField,
    required,
    SelectField,
    SelectInput,
    SimpleForm,
    SimpleShowLayout,
    TextField,
    TextInput,
    useRecordContext,
} from "react-admin";
import { CreateInDialogButton, EditInDialogButton, ShowInDialogButton } from "@react-admin/ra-form-layout";

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

const CustomerForm = (props) => (
    <SimpleForm defaultValues={{ firstname: "John", name: "Doe" }} {...props}>
        <TextInput source="first_name" validate={required()} 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) => (
    <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 }) => <>{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>.

FullyControlledCreateDialog

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) => (
    <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>
    </>
);

StackedFilters

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

StackedFilters overview

Usage

import { CreateButton, Datagrid, List, TextField, NumberField, BooleanField, ReferenceArrayField, 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 {
    CreateButton,
    Datagrid,
    List,
    TextField,
    NumberField,
    BooleanField,
    ReferenceArrayField,
    TopToolbar,
} from "react-admin";
import { StackedFilters, textFilter, numberFilter, referenceFilter, booleanFilter } from "@react-admin/ra-form-layout";

const postListFilters = {
    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>
);

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 { NumberInput } from "react-admin";
import { MyNumberRangeInput } from "./MyNumberRangeInput";

const postListFilters = {
    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 { textFilter, numberFilter, referenceFilter, booleanFilter } from "@react-admin/ra-form-layout";

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

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 DateRangeInput from "./DateRangeInput";

const MyFilterConfig = {
    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 { CreateButton, Datagrid, List, TextField, NumberField, BooleanField, ReferenceArrayField, 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 {
    CreateButton,
    Datagrid,
    List,
    TextField,
    NumberField,
    BooleanField,
    ReferenceArrayField,
    TopToolbar,
} from "react-admin";
import { StackedFilters, textFilter, numberFilter, referenceFilter, booleanFilter } from "@react-admin/ra-form-layout";

const postListFilters = {
    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>
);

<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-editable-datagrid';
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, TextField, NumberField, BooleanField, ReferenceArrayField } from "react-admin";
import {
    StackedFiltersForm,
    textFilter,
    numberFilter,
    referenceFilter,
    booleanFilter,
} from "@react-admin/ra-editable-datagrid";
import { Accordion, AccordionDetails, AccordionSummary, Card, Typography } from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";

const postListFilters = {
    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>
);

CHANGELOG

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