Input Components
An Input
component displays an input, or a dropdown list, a list of radio buttons, etc. Such components allow to edit a record property, and are common in the <Edit>
and <Create>
components, and in the List Filters.
// in src/posts.js
import * as React from "react";
import { Edit, SimpleForm, ReferenceInput, SelectInput, TextInput, required } from 'react-admin';
export const PostEdit = () => (
<Edit title={<PostTitle />}>
<SimpleForm>
<TextInput disabled source="id" />
<ReferenceInput label="User" source="userId" reference="users" validate={[required()]}>
<SelectInput optionText="name" />
</ReferenceInput>
<TextInput source="title" label="Post title" validate={[required()]} />
<TextInput multiline source="body" initialValue="Lorem Ipsum" />
</SimpleForm>
</Edit>
);
Common Input Props
All input components accept the following props:
Prop | Required | Type | Default | Description |
---|---|---|---|---|
source |
Required | string |
- | Name of the entity property to use for the input value |
label |
Optional | string |
- | Input label. In i18n apps, the label is passed to the translate function. Defaults to the humanized source when omitted. Set label={false} to hide the label. |
defaultValue |
Optional | any |
- | Default value of the input. |
validate |
Optional | Function | array |
- | Validation rules for the current property. See the Validation Documentation for details. |
helperText |
Optional | string |
- | Text to be displayed under the input |
fullWidth |
Optional | boolean |
false |
If true , the input will expand to fill the form width |
className |
Optional | string |
- | Class name (usually generated by JSS) to customize the look and feel of the field element itself |
formClassName |
Optional | string |
- | Class name to be applied to the container of the input (e.g. the <div> forming each row in <SimpleForm> ) |
sx |
Optional | SxProps |
’’ | MUI shortcut for defining custom styles with access to the theme |
<TextInput source="zb_title" label="Title" initialValue="Foo" />
React-admin uses react-hook-form to control form inputs. Each input component also accepts all react-hook-form useController hook options, with the addition of:
Prop | Required | Type | Default | Description |
---|---|---|---|---|
defaultValue |
Optional | mixed |
- | Value to be set when the property is undefined |
format |
Optional | Function |
- | Callback taking the value from the form state and the name of the field, and returns the input value. See the Transforming Input Value section. |
parse |
Optional | Function |
- | Callback taking the input value and name of the field, and returns the value you want stored in the form state. See the Transforming Input Value section. |
Additional props are passed down to the underlying component (usually a MUI component). For instance, when setting the className
prop on a TextInput
component, the underlying MUI <TextField>
receives it, and renders with custom styles. You can also set the underlying component variant
and margin
that way.
Tip: If you edit a record with a complex structure, you can use a path as the source
parameter. For instance, if the API returns the following ‘book’ record:
{
"id": 1234,
"title": "War and Peace",
"author": {
"firstName": "Leo",
"lastName": "Tolstoi"
}
}
Then you can display a text input to edit the author first name as follows:
<TextInput source="author.firstName" />
Tip: If your interface has to support multiple languages, don’t use the label
prop, and put the localized labels in a dictionary instead. See the Translation documentation for details.
Tip: For compatibility reasons, input components also accept the defaultValue
prop - which is simply copied as the initialValue
prop.
Recipes
Transforming Input Value to/from Record
The data format returned by the input component may not be what your API desires. You can use the parse
and format
functions to transform the input value when saving to and loading from the record.
Mnemonic for the two functions:
parse()
: input -> recordformat()
: record -> input
Say the user would like to input values of 0-100 to a percentage field but your API (hence record) expects 0-1.0. You can use simple parse()
and format()
functions to archive the transform:
<NumberInput source="percent" format={v => v * 100} parse={v => parseFloat(v) / 100} label="Formatted number" />
<DateInput>
stores and returns a string. If you would like to store a JavaScript Date object in your record instead:
const dateFormatRegex = /^\d{4}-\d{2}-\d{2}$/;
const dateParseRegex = /(\d{4})-(\d{2})-(\d{2})/;
const convertDateToString = value => {
// value is a `Date` object
if (!(value instanceof Date) || isNaN(value.getDate())) return '';
const pad = '00';
const yyyy = value.getFullYear().toString();
const MM = (value.getMonth() + 1).toString();
const dd = value.getDate().toString();
return `${yyyy}-${(pad + MM).slice(-2)}-${(pad + dd).slice(-2)}`;
};
const dateFormatter = value => {
// null, undefined and empty string values should not go through dateFormatter
// otherwise, it returns undefined and will make the input an uncontrolled one.
if (value == null || value === '') return '';
if (value instanceof Date) return convertDateToString(value);
// Valid dates should not be converted
if (dateFormatRegex.test(value)) return value;
return convertDateToString(new Date(value));
};
const dateParser = value => {
//value is a string of "YYYY-MM-DD" format
const match = dateParseRegex.exec(value);
if (match === null) return;
const d = new Date(match[1], parseInt(match[2], 10) - 1, match[3]);
if (isNaN(d.getDate())) return;
return d;
};
<DateInput source="isodate" format={dateFormatter} parse={dateParser} defaultValue={new Date()} />
Linking Two Inputs
Edition forms often contain linked inputs, e.g. country and city (the choices of the latter depending on the value of the former).
React-admin relies on react-hook-form for form handling. You can grab the current form values using react-hook-form useWatch hook.
import * as React from 'react';
import { Edit, SimpleForm, SelectInput } from 'react-admin';
import { useWatch } from 'react-hook-form';
const countries = ['USA', 'UK', 'France'];
const cities = {
USA: ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix'],
UK: ['London', 'Birmingham', 'Glasgow', 'Liverpool', 'Bristol'],
France: ['Paris', 'Marseille', 'Lyon', 'Toulouse', 'Nice'],
};
const toChoices = items => items.map(item => ({ id: item, name: item }));
const CityInput = props => {
const country = useWatch({ name: 'country' });
const values = getValues();
return (
<SelectInput
choices={country ? toChoices(cities[country]) : []}
{...props}
/>
);
};
const OrderEdit = () => (
<Edit>
<SimpleForm>
<SelectInput source="country" choices={toChoices(countries)} />
<CityInput source="cities" />
</SimpleForm>
</Edit>
);
export default OrderEdit;
Alternatively, you can use the react-admin <FormDataConsumer>
component, which grabs the form values, and passes them to a child function. As <FormDataConsumer>
uses the render props pattern, you can avoid creating an intermediate component like the <CityInput>
component above:
import * as React from 'react';
import { Edit, SimpleForm, SelectInput, FormDataConsumer } from 'react-admin';
const OrderEdit = () => (
<Edit>
<SimpleForm>
<SelectInput source="country" choices={toChoices(countries)} />
<FormDataConsumer>
{({ formData, ...rest }) => (
<SelectInput
source="cities"
choices={
formData.country
? toChoices(cities[formData.country])
: []
}
{...rest}
/>
)}
</FormDataConsumer>
</SimpleForm>
</Edit>
);
Tip: When using a FormDataConsumer
inside an ArrayInput
, the FormDataConsumer
will provide three additional properties to its children function:
scopedFormData
: an object containing the current values of the currently rendered item from theArrayInput
getSource
: a function which will translate the source into a valid one for theArrayInput
And here is an example usage for getSource
inside <ArrayInput>
:
import { FormDataConsumer } from 'react-admin';
const PostEdit = () => (
<Edit>
<SimpleForm>
<ArrayInput source="authors">
<SimpleFormIterator>
<TextInput source="name" />
<FormDataConsumer>
{({
formData, // The whole form data
scopedFormData, // The data for this item of the ArrayInput
getSource, // A function to get the valid source inside an ArrayInput
...rest
}) =>
scopedFormData && scopedFormData.name ? (
<SelectInput
source={getSource('role')} // Will translate to "authors[0].role"
choices={[{ id: 1, name: 'Head Writer' }, { id: 2, name: 'Co-Writer' }]}
{...rest}
/>
) : null
}
</FormDataConsumer>
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</Edit>
);
Hiding Inputs Based On Other Inputs
You may want to display or hide inputs base on the value of another input - for instance, show an email
input only if the hasEmail
boolean input has been ticked to true
.
For such cases, you can use the approach described above, using the <FormDataConsumer>
component.
import { FormDataConsumer } from 'react-admin';
const PostEdit = () => (
<Edit>
<SimpleForm>
<BooleanInput source="hasEmail" />
<FormDataConsumer>
{({ formData, ...rest }) => formData.hasEmail &&
<TextInput source="email" {...rest} />
}
</FormDataConsumer>
</SimpleForm>
</Edit>
);
Overriding The Input Variant
MUI offers 3 variants for text fields: outlined
, filled
, and standard
. The default react-admin theme uses the filled
variant. If you want your application to use another variant, override the <Admin theme>
prop with a custom theme, as follows:
import { defaultTheme } from 'react-admin';
const theme = {
...defaultTheme,
components: {
...defaultTheme.components,
MuiTextField: {
defaultProps: {
variant: 'outlined' as const,
},
},
MuiFormControl: {
defaultProps: {
variant: 'outlined' as const,
},
},
}
};
const App = () => (
<Admin theme={theme}>
// ...
</Admin>
);
Writing Your Own Input Component
If you need a more specific input type, you can write it directly in React. You’ll have to rely on react-hook-form’s useController hook, to handle the value update cycle.
Using useController
For instance, let’s write a component to edit the latitude and longitude of the current record:
// in LatLongInput.js
import { useController } from 'react-hook-form';
const LatLngInput = () => {
const input1 = useController({ name: 'lat' });
const input2 = useController({ name: 'lng' });
return (
<span>
<input {...input1.field} type="number" placeholder="latitude" />
<input {...input2.field} type="number" placeholder="longitude" />
</span>
);
};
export default LatLngInput;
// in ItemEdit.js
const ItemEdit = () => (
<Edit>
<SimpleForm>
<LatLngInput />
</SimpleForm>
</Edit>
);
LatLngInput
takes no props, because the useController
component can access the current record via the form context. The name
prop serves as a selector for the record property to edit. Executing this component will render roughly the following code:
<span>
<input name="lat" type="number" placeholder="latitude" value={record.lat} />
<input name="lng" type="number" placeholder="longitude" value={record.lng} />
</span>
Tip: React-hook-form’s useController
component supports dot notation in the name
prop, to allow binding to nested values:
import { useController } from 'react-hook-form';
const LatLngInput = () => {
const input1 = useController({ name: 'position.lat' });
const input2 = useController({ name: 'position.lng' });
return (
<span>
<input {...input1.field} type="number" placeholder="latitude" />
<input {...input2.field} type="number" placeholder="longitude" />
</span>
);
};
export default LatLngInput;
Using <Labeled>
This component lacks a label. React-admin provides the <Labeled>
component for that:
// in LatLongInput.js
import { useController } from 'react-hook-form';
import { Labeled } from 'react-admin';
const LatLngInput = () => {
const input1 = useController({ name: 'lat' });
const input2 = useController({ name: 'lng' });
return (
<Labeled label="position">
<span>
<input {...input1.field} type="number" placeholder="latitude" />
<input {...input2.field} type="number" placeholder="longitude" />
</span>
</Labeled>
);
};
export default LatLngInput;
Now the component will render with a label:
<label>Position</label>
<span>
<input name="lat" type="number" placeholder="longitude" value={record.lat} />
<input name="lng" type="number" placeholder="longitude" value={record.lng} />
</span>
Using MUI Field Components
Instead of HTML input
elements, you can use a MUI component like TextField
. To bind MUI components to the form values, use the useController()
hook:
// in LatLongInput.js
import TextField from '@mui/material/TextField';
import { useController } from 'react-hook-form';
const BoundedTextField = ({ name, label }) => {
const {
field,
fieldState: { isTouched, invalid, error },
formState: { isSubmitted }
} = useController(name);
return (
<TextField
{...field}
label={label}
error={(isTouched || isSubmitted) && invalid}
helperText={(isTouched || isSubmitted) && invalid ? error : ''}
/>
);
};
const LatLngInput = () => (
<span>
<BoundedTextField name="lat" label="latitude" />
<BoundedTextField name="lng" label="longitude" />
</span>
);
Tip: MUI’s <TextField>
component already includes a label, so you don’t need to use <Labeled>
in this case.
useController()
returns three values: field
, fieldState
, and formState
. To learn more about these props, please refer to the useController hook documentation.
Instead of HTML input
elements or MUI components, you can use react-admin input components, like <NumberInput>
for instance. React-admin components already use useController()
, and already include a label, so you don’t need either useController()
or <Labeled>
when using them:
// in LatLongInput.js
import { NumberInput } from 'react-admin';
const LatLngInput = () => (
<span>
<NumberInput source="lat" label="latitude" />
<NumberInput source="lng" label="longitude" />
</span>
);
export default LatLngInput;
The useInput()
Hook
React-admin adds functionality to react-hook-form:
- handling of custom event emitters like
onChange
, - support for an array of validators,
- detection of required fields to add an asterisk to the field label,
- parse and format to translate record values to form values and vice-versa.
So internally, react-admin components use another hook, which wraps react-hook-form’s useController()
hook. It’s called useInput()
; use it instead of useController()
to create form inputs that have the exact same API as react-admin Input components:
// in LatLongInput.js
import TextField from '@mui/material/TextField';
import { useInput, required } from 'react-admin';
const BoundedTextField = (props) => {
const { onChange, onBlur, ...rest } = props;
const {
field,
fieldState: { isTouched, invalid, error },
formState: { isSubmitted },
isRequired
} = useInput({
// Pass the event handlers to the hook but not the component as the field property already has them.
// useInput will call the provided onChange and onBlur in addition to the default needed by react-hook-form.
onChange,
onBlur,
...props,
});
return (
<TextField
{...field}
label={props.label}
error={(isTouched || isSubmitted) && invalid}
helperText={(isTouched || isSubmitted) && invalid ? error : ''}
required={isRequired}
{...rest}
/>
);
};
const LatLngInput = props => {
const {source, ...rest} = props;
return (
<span>
<BoundedTextField source="lat" label="Latitude" validate={required()} {...rest} />
<BoundedTextField source="lng" label="Longitude" validate={required()} {...rest} />
</span>
);
};
Here is another example, this time using a MUI Select
component:
// in SexInput.js
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import { useInput } from 'react-admin';
const SexInput = props => {
const {
field,
fieldState: { isTouched, invalid, error },
formState: { isSubmitted }
} = useInput(props);
return (
<Select
label="Sex"
{...field}
>
<MenuItem value="M">Male</MenuItem>
<MenuItem value="F">Female</MenuItem>
</Select>
);
};
export default SexInput;
Tip: useInput
accepts all arguments that you can pass to useController
. Besides, components using useInput
accept props like format
and parse
, to convert values from the form to the input, and vice-versa:
const parse = value => {/* ... */};
const format = value => {/* ... */};
const PersonEdit = () => (
<Edit>
<SimpleForm>
<SexInput
source="sex"
format={formValue => formValue === 0 ? 'M' : 'F'}
parse={inputValue => inputValue === 'M' ? 0 : 1}
/>
</SimpleForm>
</Edit>
);
Third-Party Components
You can find components for react-admin in third-party repositories.
- marmelab/ra-richtext-tiptap: a rich text input based on Tiptap
- vascofg/react-admin-color-input: a color input using React Color, a collection of color pickers.
- vascofg/react-admin-date-inputs: a collection of Date Inputs, based on material-ui-pickers
- MrHertal/react-admin-json-view: JSON field and input for react-admin.
- @bb-tech/ra-components:
JsonInput
which allows only valid JSON as input,JsonField
to view JSON properly on show card andTrimField
to trim the fields while showing inDatagrid
inList
component. -
@react-page/react-admin: ReactPage is a rich content editor and comes with a ready-to-use React-admin input component. check out the demo
- DEPRECATED V3 LoicMahieu/aor-tinymce-input: a TinyMCE component, useful for editing HTML