ra-form-layout
New form layouts for complex data entry tasks (accordion, wizard, etc.).
Test it live on the Enterprise Edition Storybook and in the e-commerce demo (Accordion Form, WizardForm).
Installation
npm install --save @react-admin/ra-form-layout
# or
yarn add @react-admin/ra-form-layout
Tip: ra-form-layout
is part of the React-Admin Enterprise Edition, and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package.
The package contains new translation messages (in English and French). You should add them to your i18nProvider
:
import { Admin } from 'react-admin';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
import {
raFormLayoutLanguageEnglish,
raFormLayoutLanguageFrench,
} from '@react-admin/ra-form-layout';
const messages = {
en: { ...englishMessages, ...raFormLayoutLanguageEnglish },
fr: { ...frenchMessages, ...raFormLayoutLanguageFrench },
};
const i18nProvider = polyglotI18nProvider(locale => messages[locale], 'en');
const App = () => <Admin i18nProvider={is18nProvider}>{/* ... */}</Admin>;
import { Admin } from "react-admin";
import polyglotI18nProvider from "ra-i18n-polyglot";
import englishMessages from "ra-language-english";
import frenchMessages from "ra-language-french";
import { raFormLayoutLanguageEnglish, raFormLayoutLanguageFrench } from "@react-admin/ra-form-layout";
const messages = {
en: { ...englishMessages, ...raFormLayoutLanguageEnglish },
fr: { ...frenchMessages, ...raFormLayoutLanguageFrench },
};
const i18nProvider = polyglotI18nProvider((locale) => messages[locale], "en");
const App = () => <Admin i18nProvider={is18nProvider}>{/* ... */}</Admin>;
<AccordionForm>
Alternative to <SimpleForm>
, to be used as child of <Create>
or <Edit>
. Expects <AccordionFormPanel>
elements as children.
Test it live in the e-commerce demo.
By default, each child accordion element handles its expanded state independently.
import {
Edit,
TextField,
TextInput,
DateInput,
SelectInput,
ArrayInput,
SimpleFormIterator,
BooleanInput,
} from 'react-admin';
import { AccordionForm, AccordionFormPanel } from '@react-admin/ra-form-layout';
// don't forget the component="div" prop on the main component to disable the main Card
const CustomerEdit = () => (
<Edit component="div">
<AccordionForm autoClose>
<AccordionFormPanel label="Identity">
<TextField source="id" />
<TextInput source="first_name" validate={required()} />
<TextInput source="last_name" validate={required()} />
<DateInput source="dob" label="born" validate={required()} />
<SelectInput source="sex" choices={sexChoices} />
</AccordionFormPanel>
<AccordionFormPanel label="Occupations">
<ArrayInput source="occupations" label="">
<SimpleFormIterator>
<TextInput source="name" validate={required()} />
<DateInput source="from" validate={required()} />
<DateInput source="to" />
</SimpleFormIterator>
</ArrayInput>
</AccordionFormPanel>
<AccordionFormPanel label="Preferences">
<SelectInput
source="language"
choices={languageChoices}
defaultValue="en"
/>
<BooleanInput source="dark_theme" />
<BooleanInput source="accepts_emails_from_partners" />
</AccordionFormPanel>
</AccordionForm>
</Edit>
);
import {
Edit,
TextField,
TextInput,
DateInput,
SelectInput,
ArrayInput,
SimpleFormIterator,
BooleanInput,
} from "react-admin";
import { AccordionForm, AccordionFormPanel } from "@react-admin/ra-form-layout";
// don't forget the component="div" prop on the main component to disable the main Card
const CustomerEdit = () => (
<Edit component="div">
<AccordionForm autoClose>
<AccordionFormPanel label="Identity">
<TextField source="id" />
<TextInput source="first_name" validate={required()} />
<TextInput source="last_name" validate={required()} />
<DateInput source="dob" label="born" validate={required()} />
<SelectInput source="sex" choices={sexChoices} />
</AccordionFormPanel>
<AccordionFormPanel label="Occupations">
<ArrayInput source="occupations" label="">
<SimpleFormIterator>
<TextInput source="name" validate={required()} />
<DateInput source="from" validate={required()} />
<DateInput source="to" />
</SimpleFormIterator>
</ArrayInput>
</AccordionFormPanel>
<AccordionFormPanel label="Preferences">
<SelectInput source="language" choices={languageChoices} defaultValue="en" />
<BooleanInput source="dark_theme" />
<BooleanInput source="accepts_emails_from_partners" />
</AccordionFormPanel>
</AccordionForm>
</Edit>
);
autoClose
When setting autoClose
in the <AccordionForm>
, only one accordion remains open at a time. The first accordion is open by default, and when a user opens another one, the current open accordion closes.
import { Edit, TextField, TextInput, DateInput, SelectInput, ArrayInput, SimpleFormIterator, BooleanInput } from 'react-admin';
import { AccordionForm, AccordionFormPanel } from '@react-admin/ra-form-layout';
// don't forget the component="div" prop on the main component to disable the main Card
const CustomerEdit = (props: EditProps) => (
<Edit {...props} component="div">
- <AccordionForm>
+ <AccordionForm autoClose>
<AccordionFormPanel label="Identity" defaultExpanded>
<TextField source="id" />
...
toolbar
You can customize the form Toolbar by passing a custom element in the toolbar
prop. The form expects the same type of element as <SimpleForm>
, see the <SimpleForm toolbar>
prop documentation in the react-admin docs.
<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.
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.
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.
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.
import React, { useEffect, useState } from 'react';
import { Edit, TextField } from 'react-admin';
import { LongForm } from '@react-admin/ra-form-layout';
const CustomerEditWithCardinality = () => {
const [publications, setPublications] = useState([]);
useEffect(() => {
setTimeout(() => {
setPublications([
{ id: 1, title: 'Publication 1' },
{ id: 2, title: 'Publication 2' },
{ id: 3, title: 'Publication 3' },
]);
}, 500);
}, []);
return (
<Edit component="div">
<LongForm>
<LongForm.Section label="Identity">...</LongForm.Section>
<LongForm.Section label="Occupations">...</LongForm.Section>
<LongForm.Section label="Preferences">...</LongForm.Section>
<LongForm.Section
label="Publications"
cardinality={publications.length}
>
<ul>
{publications.map(publication => (
<li key={publication.id}>
<TextField
source="title"
record={publication}
/>
</li>
))}
</ul>
</LongForm.Section>
</LongForm>
</Edit>
);
};
import React, { useEffect, useState } from "react";
import { Edit, TextField } from "react-admin";
import { LongForm } from "@react-admin/ra-form-layout";
const CustomerEditWithCardinality = () => {
const [publications, setPublications] = useState([]);
useEffect(() => {
setTimeout(() => {
setPublications([
{ id: 1, title: "Publication 1" },
{ id: 2, title: "Publication 2" },
{ id: 3, title: "Publication 3" },
]);
}, 500);
}, []);
return (
<Edit component="div">
<LongForm>
<LongForm.Section label="Identity">...</LongForm.Section>
<LongForm.Section label="Occupations">...</LongForm.Section>
<LongForm.Section label="Preferences">...</LongForm.Section>
<LongForm.Section label="Publications" cardinality={publications.length}>
<ul>
{publications.map((publication) => (
<li key={publication.id}>
<TextField source="title" record={publication} />
</li>
))}
</ul>
</LongForm.Section>
</LongForm>
</Edit>
);
};
<CreateDialog>
, <EditDialog>
& <ShowDialog>
Sometimes it makes sense to edit or create a resource without leaving the context of the list page.
For those cases, you can use the <CreateDialog>
, <EditDialog>
and <ShowDialog>
components.
They accept a single child which is the form of:
- either a
<SimpleForm>
, a<TabbedForm>
or a custom one (just like the<Create>
and<Edit>
components) for<CreateDialog>
and<EditDialog>
- either a
<SimpleShowLayout>
, a<TabbedShowLayout>
or a custom one (just like the<Show>
component) for<ShowDialog>
Basic Usage, Based On Routing
By default, the Dialog components will use the Router's location to manage their state (open or closed).
This is the easiest way to integrate them in your React-Admin app, because you don't need to manage their state manually. You only need to add them inside your List component.
Here is an example:
import React from 'react';
import {
List,
Datagrid,
SimpleForm,
SimpleShowLayout,
TextField,
TextInput,
DateInput,
DateField,
required,
ShowButton,
} from 'react-admin';
import {
EditDialog,
CreateDialog,
ShowDialog,
} from '@react-admin/ra-form-layout';
const CustomerList = () => (
<>
<List hasCreate>
<Datagrid rowClick="edit">
...
<ShowButton />
</Datagrid>
</List>
<EditDialog>
<SimpleForm>
<TextField source="id" />
<TextInput source="first_name" validate={required()} />
<TextInput source="last_name" validate={required()} />
<DateInput
source="date_of_birth"
label="born"
validate={required()}
/>
</SimpleForm>
</EditDialog>
<CreateDialog>
<SimpleForm>
<TextField source="id" />
<TextInput source="first_name" validate={required()} />
<TextInput source="last_name" validate={required()} />
<DateInput
source="date_of_birth"
label="born"
validate={required()}
/>
</SimpleForm>
</CreateDialog>
<ShowDialog>
<SimpleShowLayout>
<TextField source="id" />
<TextField source="first_name" />
<TextField source="last_name" />
<DateField source="date_of_birth" label="born" />
</SimpleShowLayout>
</ShowDialog>
</>
);
import React from "react";
import {
List,
Datagrid,
SimpleForm,
SimpleShowLayout,
TextField,
TextInput,
DateInput,
DateField,
required,
ShowButton,
} from "react-admin";
import { EditDialog, CreateDialog, ShowDialog } from "@react-admin/ra-form-layout";
const CustomerList = () => (
<>
<List hasCreate>
<Datagrid rowClick="edit">
...
<ShowButton />
</Datagrid>
</List>
<EditDialog>
<SimpleForm>
<TextField source="id" />
<TextInput source="first_name" validate={required()} />
<TextInput source="last_name" validate={required()} />
<DateInput source="date_of_birth" label="born" validate={required()} />
</SimpleForm>
</EditDialog>
<CreateDialog>
<SimpleForm>
<TextField source="id" />
<TextInput source="first_name" validate={required()} />
<TextInput source="last_name" validate={required()} />
<DateInput source="date_of_birth" label="born" validate={required()} />
</SimpleForm>
</CreateDialog>
<ShowDialog>
<SimpleShowLayout>
<TextField source="id" />
<TextField source="first_name" />
<TextField source="last_name" />
<DateField source="date_of_birth" label="born" />
</SimpleShowLayout>
</ShowDialog>
</>
);
Tip: In the example above, we added the hasCreate
prop to the <List>
component. This is necessary in order to display the "Create" button, because react-admin has no way to know that there exists a creation form for the "customer" resource otherwise.
<CreateInDialogButton>
, <EditInDialogButton>
and <ShowInDialogButton>
In some cases, you might want to use these dialog components outside the List component. For instance, you might want to have an <Edit>
view, including a <Datagrid>
, for which you would like the ability to view, edit or add records using dialog components.
For this purpose, we also provide <CreateInDialogButton>
, <EditInDialogButton>
and <ShowInDialogButton>
.
These components will create a dialog component (<CreateDialog>
, <EditDialog>
or <ShowDialog>
respectively), along with a <Button>
to open them.
These components are also responsible for creating a <FormDialogContext>
, used to manage the dialog's state (open or closed), inside which the dialog component will render.
Here is an implementation example:
import React, { ReactNode } from 'react';
import {
Datagrid,
DateField,
DateInput,
Edit,
ReferenceManyField,
required,
SelectField,
SelectInput,
SimpleForm,
SimpleShowLayout,
TextField,
TextInput,
useRecordContext,
} from 'react-admin';
import { CreateInDialogButton, EditInDialogButton, ShowInDialogButton } from '@react-admin/ra-form-layout';
const sexChoices = [
{ id: 'male', name: 'Male' },
{ id: 'female', name: 'Female' },
];
const CustomerForm = (props: any) => (
<SimpleForm defaultValues={{ firstname: 'John', name: 'Doe' }} {...props}>
<TextInput source="first_name" validate={required()} fullWidth />
<TextInput source="last_name" validate={required()} fullWidth />
<DateInput source="dob" label="born" validate={required()} fullWidth />
<SelectInput source="sex" choices={sexChoices} fullWidth />
</SimpleForm>
);
const CustomerLayout = (props: any) => (
<SimpleShowLayout {...props}>
<TextField source="first_name" fullWidth />
<TextField source="last_name" fullWidth />
<DateField source="dob" label="born" fullWidth />
<SelectField source="sex" choices={sexChoices} fullWidth />
</SimpleShowLayout>
);
// helper component to add actions buttons in a column (children),
// and also in the header (label) of a Datagrid
const DatagridActionsColumn = ({
label,
children,
}: {
label: ReactNode;
children: ReactNode;
}) => <>{children}</>;
const NestedCustomersDatagrid = () => {
const record = useRecordContext();
const createButton = (
<CreateInDialogButton
inline
fullWidth
maxWidth="md"
record={{ employer_id: record?.id }} // pre-populates the employer_id to link the new customer to the current employer
>
<CustomerForm />
</CreateInDialogButton>
);
const editButton = (
<EditInDialogButton fullWidth maxWidth="md">
<CustomerForm />
</EditInDialogButton>
);
const showButton = (
<ShowInDialogButton fullWidth maxWidth="md">
<CustomerLayout />
</ShowInDialogButton>
);
return (
<ReferenceManyField
label="Customers"
reference="customers"
target="employer_id"
>
<Datagrid>
<TextField source="id" />
<TextField source="first_name" />
<TextField source="last_name" />
<DateField source="dob" label="born" />
<SelectField source="sex" choices={sexChoices} />
{/* Using a component as label is a trick to render it in the Datagrid header */}
<DatagridActionsColumn label={createButton}>
{editButton}
{showButton}
</DatagridActionsColumn>
</Datagrid>
</ReferenceManyField>
);
};
const EmployerEdit = () => (
<Edit>
<SimpleForm>
<TextInput source="name" validate={required()} />
<TextInput source="address" validate={required()} />
<TextInput source="city" validate={required()} />
<NestedCustomersDatagrid />
</SimpleForm>
</Edit>
);
import React 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 totrue
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 stateopen
: a function that will be called when a component needs to open the dialog (e.g. a button)close
: a function that will be called when a component needs to close the dialog (e.g. the dialog's close button)
Tip: These props are exactly the same as what is stored inside a FormDialogContext
. This means that you can also rather provide your own FormDialogContext
with these values, and render your dialog component inside it, to activate standalone mode.
Below is an example of an <Edit>
page, including a 'create a new customer' button, that opens a fully controlled <CreateDialog>
.
import React, { useCallback, useState } from 'react';
import {
Button,
Datagrid,
DateField,
DateInput,
Edit,
ReferenceManyField,
required,
SelectField,
SelectInput,
SimpleForm,
TextField,
TextInput,
useRecordContext,
} from 'react-admin';
import { CreateDialog } from '@react-admin/ra-form-layout';
const sexChoices = [
{ id: 'male', name: 'Male' },
{ id: 'female', name: 'Female' },
];
const CustomerForm = (props: any) => (
<SimpleForm defaultValues={{ firstname: 'John', name: 'Doe' }} {...props}>
<TextInput source="first_name" validate={required()} fullWidth />
<TextInput source="last_name" validate={required()} fullWidth />
<DateInput source="dob" label="born" validate={required()} fullWidth />
<SelectInput source="sex" choices={sexChoices} fullWidth />
</SimpleForm>
);
const EmployerSimpleFormWithFullyControlledDialogs = () => {
const record = useRecordContext();
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const openCreateDialog = useCallback(() => {
setIsCreateDialogOpen(true);
}, []);
const closeCreateDialog = useCallback(() => {
setIsCreateDialogOpen(false);
}, []);
return (
<SimpleForm>
<TextInput source="name" validate={required()} />
<TextInput source="address" validate={required()} />
<TextInput source="city" validate={required()} />
<Button
label="Create a new customer"
onClick={() => openCreateDialog()}
size="medium"
variant="contained"
sx={{ mb: 4 }}
/>
<CreateDialog
fullWidth
maxWidth="md"
record={{ employer_id: record?.id }} // pre-populates the employer_id to link the new customer to the current employer
isOpen={isCreateDialogOpen}
open={openCreateDialog}
close={closeCreateDialog}
resource="customers"
>
<CustomerForm />
</CreateDialog>
<ReferenceManyField
label="Customers"
reference="customers"
target="employer_id"
>
<Datagrid>
<TextField source="id" />
<TextField source="first_name" />
<TextField source="last_name" />
<DateField source="dob" label="born" />
<SelectField source="sex" choices={sexChoices} />
</Datagrid>
</ReferenceManyField>
</SimpleForm>
);
};
const EmployerEdit = () => (
<Edit>
<EmployerSimpleFormWithFullyControlledDialogs />
</Edit>
);
import React, { useCallback, useState } from "react";
import {
Button,
Datagrid,
DateField,
DateInput,
Edit,
ReferenceManyField,
required,
SelectField,
SelectInput,
SimpleForm,
TextField,
TextInput,
useRecordContext,
} from "react-admin";
import { CreateDialog } from "@react-admin/ra-form-layout";
const sexChoices = [
{ id: "male", name: "Male" },
{ id: "female", name: "Female" },
];
const CustomerForm = (props) => (
<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.
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
andq
.numberFilter
: A filter for number fields. Defines the following operator:eq
,neq
,lt
andgt
.dateFilter
: A filter for date fields. Defines the following operator:eq
,neq
,lt
andgt
.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
andneq_any
.choicesArrayFilter
: A filter for array fields. Defines the following operator:inc
,inc_any
andninc_any
.referenceFilter
: A filter for reference fields. Defines the following operator:eq
,neq
,eq_any
andneq_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>
ignoresredirect
prop when passing custommutationOptions
- 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 buttondisabled
status - (doc) Fix
WizardForm
custom toolbar example
v4.5.1
2022-10-24
- (fix) Add missing exports for
CreateInDialogButton
,EditInDialogButton
andShowInDialogButton
v4.5.0
2022-10-12
- Feat: Add ability to use
CreateDialog
,EditDialog
andShowDialog
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 inra-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
withclsx
v4.1.2
2022-06-21
- Fix
<EditDialog>
not callingdataProvider.update
whenmutationMode
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 itsbasename
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 useFormGroupContext
for error tracking. - (feat) Ensure
AccordionFormPanel
,AccordionFormToolbar
andFormDialogTitle
styles are overridable through Material UI theme by providing it a key (RaAccordionFormPanel
,RaAccordionFormToolbar
andRaFormDialogTitle
).
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