Customization

Scaffolding, prefilling, linking inputs, controlled and headless modes.

Scaffolding An Edit Page

You can use <EditGuesser> to quickly bootstrap an Edit view on top of an existing API, without adding the inputs one by one.

// in src/App.js
import { Admin, EditGuesser } from "@/components/admin";
import { Resource } from "ra-core";
import dataProvider from "./data-provider";
 
const App = () => (
  <Admin dataProvider={dataProvider}>
    {/* ... */}
    <Resource name="customers" edit={EditGuesser} />
  </Admin>
);

Just like <Edit>, <EditGuesser> fetches the data. It then analyzes the response, and guesses the inputs it should use to display a basic <SimpleForm> with the data. It also dumps the components it has guessed in the console, so you can copy it into your own code.

Cleaning Up Empty Strings

As a reminder, HTML form inputs always return strings, even for numbers and booleans. So the empty value for a text input is the empty string, not null or undefined. This means that the data sent to dataProvider.update() will contain empty strings:

{
    title: '',
    average_note: '',
    body: '',
    // etc.
}

If you prefer to have null values, or to omit the key for empty values, use the transform prop to sanitize the form data before submission:

export const UserEdit = () => {
  const transform = (data) => {
    const sanitizedData = {};
    for (const key in data) {
      if (typeof data[key] === "string" && data[key].trim().length === 0)
        continue;
      sanitizedData[key] = data[key];
    }
    return sanitizedData;
  };
  return <Edit transform={transform}>...</Edit>;
};

As an alternative, you can clean up empty values at the input level, using the parse prop.

Prefilling the Form

You sometimes need to pre-populate the form changes to a record. For instance, to revert a record to a previous version, or to make some changes while letting users modify others fields as well.

By default, the <Edit> view starts with the current record. However, if the location object (injected by react-router-dom) contains a record in its state, the <Edit> view uses that record to prefill the form.

That means that if you want to create a link to an edition view, modifying immediately some values, all you have to do is to set the state prop of the <EditButton>:

import * as React from "react";
import { EditButton, DataTable, List } from "@/components/admin";
 
const ApproveButton = () => (
  <EditButton state={{ record: { status: "approved" } }} />
);
 
export default PostList = () => (
  <List>
    <DataTable>
      ...
      <DataTable.Col>
        <ApproveButton />
      </DataTable.Col>
    </DataTable>
  </List>
);

Should you use the location state or the location search? The latter modifies the URL, so it's only necessary if you want to build cross-application links (e.g. from one admin to the other). In general, using the location state is a safe bet.

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

Shadmin relies on react-hook-form for form handling. You can grab the current form values using react-hook-form's useWatch hook.

import { Edit, SimpleForm, SelectInput } from "@/components/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 = () => {
  const country = useWatch({ name: "country" });
  return (
    <SelectInput
      choices={country ? toChoices(cities[country]) : []}
      source="cities"
    />
  );
};
 
const OrderEdit = () => (
  <Edit>
    <SimpleForm>
      <SelectInput source="country" choices={toChoices(countries)} />
      <CityInput />
    </SimpleForm>
  </Edit>
);
 
export default OrderEdit;

Controlled Mode

<Edit> deduces the resource and the record id from the URL. This is fine for an edition page, but if you need to let users edit records from another page, you probably want to define the edit parameters yourself.

In that case, use the resource and id props to set the edit parameters regardless of the URL.

import { Edit, SimpleForm, TextInput, SelectInput } from "@/components/admin";
 
export const BookEdit = ({ id }) => (
  <Edit resource="books" id={id} redirect={false}>
    <SimpleForm>
      <TextInput source="title" />
      <TextInput source="author" />
      <SelectInput
        source="availability"
        choices={[
          { id: "in_stock", name: "In stock" },
          { id: "out_of_stock", name: "Out of stock" },
          { id: "out_of_print", name: "Out of print" },
        ]}
      />
    </SimpleForm>
  </Edit>
);

Headless Version

Besides fetching a record and preparing a save handler, <Edit> renders the default edition page layout (title, actions, a Material UI <Card>) and its children. If you need a custom edition layout, you may prefer the <EditBase> component, which only renders its children in an EditContext.

import { EditBase } from "ra-core";
import { SelectInput, SimpleForm, TextInput } from "@/components/admin";
import { Card, CardContent } from "@/commponents/ui";
 
export const BookEdit = () => (
  <EditBase>
    <h1>Book Edition</h1>
    <Card>
      <CardContent>
        <SimpleForm>
          <TextInput source="title" />
          <TextInput source="author" />
          <SelectInput
            source="availability"
            choices={[
              { id: "in_stock", name: "In stock" },
              { id: "out_of_stock", name: "Out of stock" },
              { id: "out_of_print", name: "Out of print" },
            ]}
          />
        </SimpleForm>
      </CardContent>
    </Card>
  </EditBase>
);

In the previous example, <SimpleForm> grabs the record and the save handler from the EditContext.

If you don't need the EditContext, you can use the useEditController hook, which does the same data fetching as <EditBase> but lets you render the content.

import { useEditController } from "ra-core";
import { SelectInput, SimpleForm, TextInput } from "@/components/admin";
import { Card, CardContent } from "@/components/ui";
 
export const BookEdit = () => {
  const { record, save } = useEditController();
  return (
    <>
      <h1>Edit book {record?.title}</h1>
      <Card>
        <CardContent>
          <SimpleForm record={record} onSubmit={(values) => save(values)}>
            <TextInput source="title" />
            <TextInput source="author" />
            <SelectInput
              source="availability"
              choices={[
                { id: "in_stock", name: "In stock" },
                { id: "out_of_stock", name: "Out of stock" },
                { id: "out_of_print", name: "Out of print" },
              ]}
            />
          </SimpleForm>
        </CardContent>
      </Card>
    </>
  );
};