4.2.0

react-admin ≥ 4.1.2

ra-form-layout

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 <SimpleForm> 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 React from "react";
import { Create, TextInput, required } from "react-admin";
import {
  WizardForm,
  WizardFormStep,
  WizardToolbarProps,
} from "@react-admin/ra-form-layout";

const MyToolbar = ({
  hasPreviousStep,
  hasNextStep,
  onPreviousClick,
  onNextClick,
  handleSubmit,
  handleSubmitWithRedirect,
  invalid,
  redirect,
  saving,
  submitOnEnter,
}: WizardToolbarProps) => {
  const save = handleSubmitWithRedirect || handleSubmit;

  return (
    <ul>
      {hasPreviousStep ? (
        <li>
          <Button onClick={onPreviousClick}>PREVIOUS</Button>
        </li>
      ) : null}
      {hasNextStep ? (
        <li>
          <Button disabled={invalid} onClick={onNextClick}>
            NEXT
          </Button>
        </li>
      ) : (
        <li>
          <Button disabled={invalid} 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 React from "react";
import { Create, TextInput, required } from "react-admin";
import { WizardForm, WizardFormStep } from "@react-admin/ra-form-layout";

const MyToolbar = ({
  hasPreviousStep,
  hasNextStep,
  onPreviousClick,
  onNextClick,
  handleSubmit,
  handleSubmitWithRedirect,
  invalid,
  redirect,
  saving,
  submitOnEnter,
}) => {
  const save = handleSubmitWithRedirect || handleSubmit;

  return (
    <ul>
      {hasPreviousStep ? (
        <li>
          <Button onClick={onPreviousClick}>PREVIOUS</Button>
        </li>
      ) : null}
      {hasNextStep ? (
        <li>
          <Button disabled={invalid} onClick={onNextClick}>
            NEXT
          </Button>
        </li>
      ) : (
        <li>
          <Button disabled={invalid} 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> and <EditDialog> components.

EditDialog

They accept a single child which is the form, either a <SimpleForm>, a <TabbedForm> or a custom one, just like the <Create> and <Edit> components.

import React from "react";
import {
  List,
  Datagrid,
  SimpleForm,
  SimpleShowLayout,
  TextField,
  TextInput,
  DateInput,
  DateField,
  required,
} 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,
} 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.

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

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

const CustomerEditTitle = ({ record }) =>
  record ? (
    <span>
      {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="Show customer">
      <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,
} from "react-admin";
import {
  EditDialog,
  CreateDialog,
  ShowDialog,
} from "@react-admin/ra-form-layout";

const CustomerEditTitle = ({ record }) =>
  record ? (
    <span>
      {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="Show customer">
      <SimpleShowLayout>
        <TextField source="id" />
        <TextField source="first_name" />
        <TextField source="last_name" />
        <DateField source="date_of_birth" label="born" />
      </SimpleShowLayout>
    </ShowDialog>
  </>
);

Besides, you can also pass the props accepted by the MUI <Dialog> like fullWidth or maxWidth.

import React from "react";
import {
  List,
  Datagrid,
  SimpleForm,
  SimpleShowLayout,
  TextField,
  TextInput,
  DateInput,
  DateField,
  required,
} 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,
} 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>
  </>
);

<JsonSchemaForm>

If you have a JSON Schema description of your form based on react-jsonschema-form, you can use the <JsonSchemaForm> component to render it.

For instance, to generate the following form:

json schema form

Configure the <Edit> view with a <JsonSchemaForm> child as follows:

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

const CustomerEdit = () => (
  <Edit>
    <JsonSchemaForm
      schema={{
        type: "object",
        properties: {
          id: { type: "number" },
          first_name: { type: "string", title: "First name" },
          last_name: { type: "string", minLength: 3 },
          dob: { type: "string", format: "date" },
          sex: { type: "string", enum: ["male", "female"] },
          employer_id: { type: "number" },
          occupations: {
            type: "array",
            items: {
              type: "object",
              properties: {
                name: { type: "string" },
                from: { type: "string", format: "date" },
                to: { type: "string", format: "date" },
              },
            },
          },
        },
        required: ["id", "last_name", "employer_id"],
      }}
      uiSchema={{
        id: { "ui:disabled": true },
        employer_id: {
          "ui:widget": "reference",
          "ui:options": {
            reference: "employers",
            optionText: "name",
          },
        },
      }}
      onChange={(change) =>
        process.env.NODE_ENV !== "test" && console.log("changed", change)
      }
      onError={(error) =>
        process.env.NODE_ENV !== "test" && console.log("error", error)
      }
    />
  </Edit>
);
import { Edit } from "react-admin";
import { JsonSchemaForm } from "@react-admin/ra-form-layout";

const CustomerEdit = () => (
  <Edit>
    <JsonSchemaForm
      schema={{
        type: "object",
        properties: {
          id: { type: "number" },
          first_name: { type: "string", title: "First name" },
          last_name: { type: "string", minLength: 3 },
          dob: { type: "string", format: "date" },
          sex: { type: "string", enum: ["male", "female"] },
          employer_id: { type: "number" },
          occupations: {
            type: "array",
            items: {
              type: "object",
              properties: {
                name: { type: "string" },
                from: { type: "string", format: "date" },
                to: { type: "string", format: "date" },
              },
            },
          },
        },
        required: ["id", "last_name", "employer_id"],
      }}
      uiSchema={{
        id: { "ui:disabled": true },
        employer_id: {
          "ui:widget": "reference",
          "ui:options": {
            reference: "employers",
            optionText: "name",
          },
        },
      }}
      onChange={(change) =>
        process.env.NODE_ENV !== "test" && console.log("changed", change)
      }
      onError={(error) =>
        process.env.NODE_ENV !== "test" && console.log("error", error)
      }
    />
  </Edit>
);

<JsonSchemaForm> initializes the form with the current record, and renders it like <SimpleForm> does.

It expects a schema prop describing the expected data shape, and a uiSchema prop describing the UI.

<JsonSchemaForm> is a wrapper around JsonSchema Form's <Form> component, so please refer to JsonSchema Form's documentation for detailed usage.

<JsonSchemaForm> comes with the following UI widgets:

For boolean fields:

  • checkbox (default)
  • radio
  • select

For string fields:

  • text (default)
  • textarea
  • password
  • color

The built-in string field also supports the JSON Schema format property, and will render an appropriate widget accordingly:

  • email: An input[type=email] element is used;
  • uri: An input[type=url] element is used;
  • data-url: By default, an input[type=file] element is used; in case the string is part of an array, multiple files will be handled automatically .
  • date: By default, an input[type=date] element is used;
  • date-time: By default, an input[type=datetime-local] element is used.

For number and integer fields:

  • text (default)
  • updown
  • range
  • radio

ra-form-layout comes with the an additional UI widget for string fields: reference. It's the equivalent of react-admin's <ReferenceInput> component. It fetches the foreign key, and uses a relationship to populate the list of options.

Specify the reference, optionText, and other options through the ui:options UI schema directive:

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

const CustomerEdit = () => (
  <Edit>
    <JsonSchemaForm
      schema={{
        type: "object",
        properties: {
          id: { type: "number" },
          employer_id: { type: "number" },
        },
      }}
      uiSchema={{
        employer_id: {
          "ui:widget": "reference",
          "ui:options": {
            reference: "employers",
            optionText: "name",
          },
        },
      }}
    />
  </Edit>
);
import { Edit } from "react-admin";
import { JsonSchemaForm } from "@react-admin/ra-form-layout";

const CustomerEdit = () => (
  <Edit>
    <JsonSchemaForm
      schema={{
        type: "object",
        properties: {
          id: { type: "number" },
          employer_id: { type: "number" },
        },
      }}
      uiSchema={{
        employer_id: {
          "ui:widget": "reference",
          "ui:options": {
            reference: "employers",
            optionText: "name",
          },
        },
      }}
    />
  </Edit>
);

CHANGELOG

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