Skip to content

Commit

Permalink
Merge pull request #9447 from marmelab/doc-headless
Browse files Browse the repository at this point in the history
[Doc] Add headless section in pages components
djhi authored Nov 22, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents cb1941a + 68dcf9e commit d1042fd
Showing 18 changed files with 817 additions and 87 deletions.
89 changes: 89 additions & 0 deletions docs/Create.md
Original file line number Diff line number Diff line change
@@ -50,6 +50,8 @@ const App = () => (
export default App;
```

## Props

You can customize the `<Create>` component using the following props:

* [`actions`](#actions): override the actions toolbar with a custom component
@@ -599,3 +601,90 @@ export default OrderEdit;
```

**Tip:** If you'd like to avoid creating an intermediate component like `<CityInput>`, or are using an `<ArrayInput>`, you can use the [`<FormDataConsumer>`](./Inputs.md#linking-two-inputs) component as an alternative.

## Controlled Mode

`<Create>` deduces the resource and the initial form values from the URL. This is fine for a creation page, but if you need to let users create records from another page, you probably want to define this parameter yourself.

In that case, use the [`resource`](#resource) and [`record`](#record) props to set the creation parameters regardless of the URL.

```jsx
import { Create, SimpleForm, TextInput, SelectInput } from "react-admin";

export const BookCreate = () => (
<Create resource="books" 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>
</Create>
);
```

**Tip**: You probably also want to customize [the `redirect` prop](#redirect) if you embed an `<Create>` component in another page.

## Headless Version

Besides preparing a save handler, `<Create>` renders the default creation page layout (title, actions, a Material UI `<Card>`) and its children. If you need a custom creation layout, you may prefer [the `<CreateBase>` component](./CreateBase.md), which only renders its children in a [`CreateContext`](./useCreateContext.md).

```jsx
import { CreateBase, SelectInput, SimpleForm, TextInput, Title } from "react-admin";
import { Card, CardContent, Container } from "@mui/material";

export const BookCreate = () => (
<CreateBase>
<Container>
<Title title="Create book" />
<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>
</Container>
</CreateBase>
);
```

In the previous example, `<SimpleForm>` grabs the save handler from the `CreateContext`.

If you don't need the `CreateContext`, you can use [the `useCreateController` hook](./useCreateController.md), which does the same data fetching as `<CreateBase>` but lets you render the content.

```jsx
import { useCreateController, SelectInput, SimpleForm, TextInput, Title } from "react-admin";
import { Card, CardContent, Container } from "@mui/material";

export const BookCreate = () => {
const { save } = useCreateController();
return (
<Container>
<Title title="Create book" />
<Card>
<CardContent>
<SimpleForm 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>
</Container>
);
};
```
6 changes: 4 additions & 2 deletions docs/CreateBase.md
Original file line number Diff line number Diff line change
@@ -5,9 +5,11 @@ title: "The CreateBase Component"

# `<CreateBase>`

The `<CreateBase>` component is a headless version of `<Create>`: it prepares a form submit handler, and renders its children.
`<CreateBase>` is a headless variant of [`<Create>`](./Create.md). It prepares a form submit handler, and renders its children in a [`CreateContext`](./useCreateContext.md). Use it to build a custom creation page layout.

It does that by calling `useCreateController`, and by putting the result in an `CreateContext`.
Contrary to [`<Create>`](./Create.md), it does not render the page layout, so no title, no actions, and no `<Card>`.

`<CreateBase>` relies on the [`useCreateController`](./useCreateController.md) hook.

## Usage

89 changes: 89 additions & 0 deletions docs/Edit.md
Original file line number Diff line number Diff line change
@@ -58,6 +58,8 @@ const App = () => (
export default App;
```

## Props

You can customize the `<Edit>` component using the following props:

* [`actions`](#actions): override the actions toolbar with a custom component
@@ -770,3 +772,90 @@ export const PostEdit = () => (
```

**Tips:** If you want users to be warned if they haven't pressed the Save button when they browse to another record, you can follow the tutorial [Navigating Through Records In`<Edit>` Views](./PrevNextButtons.md#navigating-through-records-in-edit-views-after-submit).

## 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`](#resource) and [`id`](#id) props to set the edit parameters regardless of the URL.

```jsx
import { Edit, SimpleForm, TextInput, SelectInput } from "react-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>
);
```

**Tip**: You probably also want to customize [the `redirect` prop](#redirect) if you embed an `<Edit>` component in another page.

## 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](./EditBase.md), which only renders its children in an [`EditContext`](./useEditContext.md).

```jsx
import { EditBase, SelectInput, SimpleForm, TextInput, Title } from "react-admin";
import { Card, CardContent, Container } from "@mui/material";

export const BookEdit = () => (
<EditBase>
<Container>
<Title title="Book Edition" />
<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>
</Container>
</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](./useEditController.md), which does the same data fetching as `<EditBase>` but lets you render the content.

```tsx
import { useEditController, SelectInput, SimpleForm, TextInput, Title } from "react-admin";
import { Card, CardContent, Container } from "@mui/material";

export const BookEdit = () => {
const { record, save } = useEditController();
return (
<Container>
<Title title={`Edit book ${record?.title}`} />
<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>
</Container>
);
};
```
37 changes: 21 additions & 16 deletions docs/EditBase.md
Original file line number Diff line number Diff line change
@@ -5,39 +5,44 @@ title: "The EditBase Component"

# `<EditBase>`

The `<EditBase>` component is a headless version of [`<Edit>`](./Edit.md): it fetches a record based on the URL, prepares a form submit handler, and renders its children.
`<EditBase>` is a headless variant of [`<Edit>`](./Edit.md): it fetches a record based on the URL, prepares a form submit handler, and renders its children inside an [`EditContext`](./useEditContext.md). Use it to build a custom edition page layout.

It does that by calling [`useEditController`](./useEditController.md), and by putting the result in an `EditContext`.
Contrary to [`<Edit>`](./Edit.md), it does not render the page layout, so no title, no actions, and no `<Card>`.

`<EditBase>` relies on the [`useEditController`](./useEditController.md) hook.

## Usage

Use `<EditBase>` to create a custom Edition view, with exactly the content you add as child and nothing else (no title, Card, or list of actions as in the `<Edit>` component).

```jsx
import * as React from "react";
import { EditBase, SimpleForm, TextInput, SelectInput } from "react-admin";
import { Card } from "@mui/material";
import { EditBase, SelectInput, SimpleForm, TextInput, Title } from "react-admin";
import { Card, CardContent, Container } from "@mui/material";

export const BookEdit = () => (
<EditBase>
<div>
<Container>
<Title title="Book Edition" />
<Card>
<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>
<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>
</div>
</Container>
</EditBase>
);
```

## Props

You can customize the `<EditBase>` component using the following props, documented in the `<Edit>` component:

* `children`: the components that renders the form
87 changes: 87 additions & 0 deletions docs/Features.md
Original file line number Diff line number Diff line change
@@ -271,6 +271,93 @@ We have made many improvements to this default layout based on user feedback. In

And for mobile users, react-admin renders a different layout with larger margins and less information density (see [Responsive](#responsive)).

## Headless

React-admin components use Material UI components by default, which lets you scaffold a page in no time. As material UI supports [theming](#theming), you can easily customize the look and feel of your app. But in some cases, this is not enough, and you need to use another UI library.

You can change the UI library you use with react-admin to use [Ant Design](https://ant.design/), [Daisy UI](https://daisyui.com/), [Chakra UI](https://chakra-ui.com/), or even you own custom UI library. The **headless logic** behind react-admin components is agnostic of the UI library, and is exposed via `...Base` components and controller hooks.

For instance, here a List view built with [Ant Design](https://ant.design/):

![List view built with Ant Design](./img/list_ant_design.png)

It leverages the `useListController` hook:

{% raw %}
```jsx
import { useListController } from 'react-admin';
import { Card, Table, Button } from 'antd';
import {
CheckCircleOutlined,
PlusOutlined,
EditOutlined,
} from '@ant-design/icons';
import { Link } from 'react-router-dom';

const PostList = () => {
const { data, page, total, setPage, isLoading } = useListController({
sort: { field: 'published_at', order: 'DESC' },
perPage: 10,
});
const handleTableChange = (pagination) => {
setPage(pagination.current);
};
return (
<>
<div style={{ margin: 10, textAlign: 'right' }}>
<Link to="/posts/create">
<Button icon={<PlusOutlined />}>Create</Button>
</Link>
</div>
<Card bodyStyle={{ padding: '0' }} loading={isLoading}>
<Table
size="small"
dataSource={data}
columns={columns}
pagination={{ current: page, pageSize: 10, total }}
onChange={handleTableChange}
/>
</Card>
</>
);
};

const columns = [
{ title: 'Id', dataIndex: 'id', key: 'id' },
{ title: 'Title', dataIndex: 'title', key: 'title' },
{
title: 'Publication date',
dataIndex: 'published_at',
key: 'pub_at',
render: (value) => new Date(value).toLocaleDateString(),
},
{
title: 'Commentable',
dataIndex: 'commentable',
key: 'commentable',
render: (value) => (value ? <CheckCircleOutlined /> : null),
},
{
title: 'Actions',
render: (_, record) => (
<Link to={`/posts/${record.id}`}>
<Button icon={<EditOutlined />}>Edit</Button>
</Link>
),
},
];

export default PostList;
```
{% endraw %}

Check the following hooks to learn more about headless controllers:

- [`useListController`](./useListController.md)
- [`useEditController`](./useEditController.md)
- [`useCreateController`](./useCreateController.md)
- [`useShowController`](./useShowController.md)

## Guessers & Scaffolding

When mapping a new API route to a CRUD view, adding fields one by one can be tedious. React-admin provides a set of guessers that can automatically **generate a complete CRUD UI based on an API response**.
4 changes: 3 additions & 1 deletion docs/Form.md
Original file line number Diff line number Diff line change
@@ -5,10 +5,12 @@ title: "Form"

# `<Form>`

The `<Form>` component creates a `<form>` to edit a record, and renders its children. It is a headless component used internally by `<SimpleForm>`, `<TabbedForm>`, and other form components.
`<Form>` is a headless component that creates a `<form>` to edit a record, and renders its children. Use it to build a custom form layout, or to use another UI kit than Material UI.

`<Form>` reads the `record` from the `RecordContext`, uses it to initialize the defaultValues of a react-hook-form via `useForm`, turns the `validate` function info a react-hook-form compatible form validator, notifies the user when the input validation fails, and creates a form context via `<FormProvider>`.

`<Form>` is used internally by `<SimpleForm>`, `<TabbedForm>`, and other form components.

## Usage

Use `<Form>` to build completely custom form layouts. Don't forget to include a submit button (or react-admin's [`<SaveButton>`](./SaveButton.md)) to actually save the record.
109 changes: 108 additions & 1 deletion docs/InfiniteList.md
Original file line number Diff line number Diff line change
@@ -161,4 +161,111 @@ export const BookList = () => (
</InfiniteList>
);
```
{% endraw %}
{% endraw %}

## Controlled Mode

`<InfiniteList>` deduces the resource and the list parameters from the URL. This is fine for a page showing a single list of records, but if you need to display more than one list in a page, you probably want to define the list parameters yourself.

In that case, use the [`resource`](#resource), [`sort`](#sort), and [`filter`](#filter-permanent-filter) props to set the list parameters.

{% raw %}
```jsx
import { InfiniteList, InfinitePagination, SimpleList } from 'react-admin';
import { Container, Typography } from '@mui/material';

const Dashboard = () => (
<Container>
<Typography>Latest posts</Typography>
<InfiniteList
resource="posts"
sort={{ field: 'published_at', order: 'DESC' }}
filter={{ is_published: true }}
disableSyncWithLocation
>
<SimpleList
primaryText={record => record.title}
secondaryText={record => `${record.views} views`}
/>
<InfinitePagination />
</InfiniteList>
<Typography>Latest comments</Typography>
<InfiniteList
resource="comments"
sort={{ field: 'published_at', order: 'DESC' }}
perPage={10}
disableSyncWithLocation
>
<SimpleList
primaryText={record => record.author.name}
secondaryText={record => record.body}
tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
/>
<InfinitePagination />
</InfiniteList>
</Container>
)
```
{% endraw %}

## Headless Version

Besides fetching a list of records from the data provider, `<InfiniteList>` renders the default list page layout (title, buttons, filters, a Material-UI `<Card>`, infinite pagination) and its children. If you need a custom list layout, you may prefer the `<InfiniteListBase>` component, which only renders its children in a [`ListContext`](./useListContext.md).

```jsx
import { InfiniteListBase, InfinitePagination, WithListContext } from 'react-admin';
import { Card, CardContent, Container, Stack, Typography } from '@mui/material';

const ProductList = () => (
<InfiniteListBase>
<Container>
<Typography variant="h4">All products</Typography>
<WithListContext render={({ isLoading, data }) => (
!isLoading && (
<Stack spacing={1}>
{data.map(product => (
<Card key={product.id}>
<CardContent>
<Typography>{product.name}</Typography>
</CardContent>
</Card>
))}
</Stack>
)
)} />
<InfinitePagination />
</Container>
</InfiniteListBase>
);
```

The previous example leverages [`<WithListContext>`](./WithListContext.md) to grab the data that `<ListBase>` stores in the `ListContext`.

If you don't need the `ListContext`, you can use the `useInfiniteListController` hook, which does the same data fetching as `<InfiniteListBase>` but lets you render the content.

```jsx
import { useInfiniteListController } from 'react-admin';
import { Card, CardContent, Container, Stack, Typography } from '@mui/material';

const ProductList = () => {
const { isLoading, data } = useInfiniteListController();
return (
<Container>
<Typography variant="h4">All products</Typography>
{!isLoading && (
<Stack spacing={1}>
{data.map(product => (
<Card key={product.id}>
<CardContent>
<Typography>{product.name}</Typography>
</CardContent>
</Card>
))}
</Stack>
)}
</Container>
);
};
```

`useInfiniteListController` returns callbacks to sort, filter, and paginate the list, so you can build a complete List page.
155 changes: 155 additions & 0 deletions docs/List.md
Original file line number Diff line number Diff line change
@@ -1081,3 +1081,158 @@ const ProductList = () => (
</List>
)
```

## Controlled Mode

`<List>` deduces the resource and the list parameters from the URL. This is fine for a page showing a single list of records, but if you need to display more than one list in a page, you probably want to define the list parameters yourself.

In that case, use the [`resource`](#resource), [`sort`](#sort), [`filter`](#filter-permanent-filter), and [`perPage`](#perpage) props to set the list parameters.

{% raw %}
```jsx
import { List, SimpleList } from 'react-admin';
import { Container, Typography } from '@mui/material';

const Dashboard = () => (
<Container>
<Typography>Latest posts</Typography>
<List
resource="posts"
sort={{ field: 'published_at', order: 'DESC' }}
filter={{ is_published: true }}
perPage={10}
>
<SimpleList
primaryText={record => record.title}
secondaryText={record => `${record.views} views`}
/>
</List>
<Typography>Latest comments</Typography>
<List
resource="comments"
sort={{ field: 'published_at', order: 'DESC' }}
perPage={10}
>
<SimpleList
primaryText={record => record.author.name}
secondaryText={record => record.body}
tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
/>
</List>
</Container>
)
```
{% endraw %}

**Note**: If you need to set the list parameters to render a list of records *related to another record*, there are better components than `<List>` for that. Check out the following components, specialized in fetching and displaying a list of related records:

- [`<ReferenceArrayField>`](./ReferenceArrayField.md),
- [`<ReferenceManyField>`](./ReferenceManyField.md),
- [`<ReferenceManyToManyField>`](./ReferenceManyToManyField.md).

If the `<List>` children allow to *modify* the list state (i.e. if they let users change the sort order, the filters, the selection, or the pagination), then you should also use the [`disableSyncWithLocation`](#disablesyncwithlocation) prop to prevent react-admin from changing the URL. This is the case e.g. if you use a `<Datagrid>`, which lets users sort the list by clicking on column headers.

{% raw %}
```jsx
import { List, Datagrid, TextField, NumberField, DateField } from 'react-admin';
import { Container, Typography } from '@mui/material';

const Dashboard = () => (
<Container>
<Typography>Latest posts</Typography>
<List
resource="posts"
sort={{ field: 'published_at', order: 'DESC' }}
filter={{ is_published: true }}
perPage={10}
disableSyncWithLocation
>
<Datagrid bulkActionButtons={false}>
<TextField source="title" />
<NumberField source="views" />
</Datagrid>
</List>
<Typography>Latest comments</Typography>
<List
resource="comments"
sort={{ field: 'published_at', order: 'DESC' }}
perPage={10}
disableSyncWithLocation
>
<Datagrid bulkActionButtons={false}>
<TextField source="author.name" />
<TextField source="body" />
<DateField source="published_at" />
</Datagrid>
</List>
</Container>
)
```
{% endraw %}

**Note**: If you render more than one `<Datagrid>` for the same resource in the same page, they will share the selection state (i.e. the checked checkboxes). This is a design choice because if row selection is not tied to a resource, then when a user deletes a record it may remain selected without any ability to unselect it. You can get rid of the checkboxes by setting `<Datagrid bulkActionButtons={false}>`.

## Headless Version

Besides fetching a list of records from the data provider, `<List>` renders the default list page layout (title, buttons, filters, a Material-UI `<Card>`, pagination) and its children. If you need a custom list layout, you may prefer [the `<ListBase>` component](./ListBase.md), which only renders its children in a [`ListContext`](./useListContext.md).

```jsx
import { ListBase, WithListContext } from 'react-admin';
import { Card, CardContent, Container, Stack, Typography } from '@mui/material';

const ProductList = () => (
<ListBase>
<Container>
<Typography variant="h4">All products</Typography>
<WithListContext render={({ isLoading, data }) => (
!isLoading && (
<Stack spacing={1}>
{data.map(product => (
<Card key={product.id}>
<CardContent>
<Typography>{product.name}</Typography>
</CardContent>
</Card>
))}
</Stack>
)
)} />
<WithListContext render={({ isLoading, total }) => (
!isLoading && <Typography>{total} results</Typography>
)} />
</Container>
</ListBase>
);
```

The previous example leverages [`<WithListContext>`](./WithListContext.md) to grab the data that `<ListBase>` stores in the `ListContext`.

If you don't need the `ListContext`, you can use [the `useListController` hook](./useListController.md), which does the same data fetching as `<ListBase>` but lets you render the content.

```jsx
import { useListController } from 'react-admin';
import { Card, CardContent, Container, Stack, Typography } from '@mui/material';

const ProductList = () => {
const { isLoading, data, total } = useListController();
return (
<Container>
<Typography variant="h4">All products</Typography>
{!isLoading && (
<Stack spacing={1}>
{data.map(product => (
<Card key={product.id}>
<CardContent>
<Typography>{product.name}</Typography>
</CardContent>
</Card>
))}
</Stack>
)}
{!isLoading && <Typography>{total} results</Typography>}
</Container>
);
};
```

`useListController` returns callbacks to sort, filter, and paginate the list, so you can build a complete List page. Check [the `useListController`hook documentation](./useListController.md) for details.
6 changes: 4 additions & 2 deletions docs/ListBase.md
Original file line number Diff line number Diff line change
@@ -5,9 +5,11 @@ title: "The ListBase Component"

# `<ListBase>`

`<ListBase>` is a headless variant of `<List>`, as it does not render anything. `<ListBase>` calls [`useListController`](./useListController.md), puts the result in a `ListContext`, then renders its child.
`<ListBase>` is a headless variant of [`<List>`](./List.md). It fetches a list of records from the data provider, puts it in a [`ListContext`](./useListContext.md), and renders its children. Use it to build a custom list layout.

If you want to display a record list in an entirely custom layout, i.e. use only the data fetching part of `<List>` and not the view layout, use `<ListBase>`.
Contrary to [`<List>`](./List.md), it does not render the page layout, so no title, no actions, no `<Card>`, and no pagination.

`<ListBase>` relies on the [`useListController`](./useListController.md) hook.

## Usage

94 changes: 75 additions & 19 deletions docs/Show.md
Original file line number Diff line number Diff line change
@@ -604,31 +604,87 @@ export const PostShow = () => (

**Tips:** If you want the `<PrevNextButtons>` to link to the `<Show>` view, you have to set the `linkType` to `show`. See [the `<PrevNextButtons linkType>` prop](./PrevNextButtons.md#linktype).

## Headless Version
## Controlled Mode

The root component of `<Show>` is a Material UI `<Card>`. Besides, `<Show>` renders an action toolbar, and sets the page title. This may be useless if you have a completely custom layout.
`<show>` deduces the resource and the record id from the URL. This is fine for a detail page, but if you need to embed the details of a record in another page, you probably want to define these parameters yourself.

In that case, opt for [the `<ShowBase>` component](./ShowBase.md), a headless version of `<Show>`.
In that case, use the [`resource`](#resource) and [`id`](#id) props to set the show parameters regardless of the URL.

```jsx
// in src/posts.jsx
import { ShowBase } from 'react-admin';
import { Show, SelectField, SimpleShowLayout, TextField } from "react-admin";

export const PostShow = () => (
export const BookShow = ({ id }) => (
<Show resource="books" id={id}>
<SimpleShowLayout>
<TextField source="title" />
<TextField source="author" />
<SelectField 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" },
]} />
</SimpleShowLayout>
</Show>
);
```

## Headless Version

Besides fetching a record, `<Show>` renders the default detail page layout (title, actions, a Material UI `<Card>`) and its children. If you need a custom detail layout, you may prefer [the `<ShowBase>` component](./ShowBase.md), which only renders its children in a [`ShowContext`](./useShowContext.md).

```jsx
import { ShowBase, SelectField, SimpleShowLayout, TextField, Title } from "react-admin";
import { Card, CardContent, Container } from "@mui/material";

export const BookShow = () => (
<ShowBase>
<Grid container>
<Grid item xs={8}>
<SimpleShowLayout>
...
</SimpleShowLayout>
</Grid>
<Grid item xs={4}>
Show instructions...
</Grid>
</Grid>
<div>
Post related links...
</div>
<Container>
<Title title="Book Detail" />
<Card>
<CardContent>
<SimpleShowLayout>
<TextField source="title" />
<TextField source="author" />
<SelectField 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" },
]} />
</SimpleShowLayout>
</CardContent>
</Card>
</Container>
</ShowBase>
);
```

In the previous example, `<SimpleShowLayout>` grabs the record from the `ShowContext`.

If you don't need the `ShowContext`, you can use [the `useShowController` hook](./useShowController.md), which does the same data fetching as `<ShowBase>` but lets you render the content.

```tsx
import { useShowController, SelectField, SimpleShowLayout, TextField, Title } from "react-admin";
import { Card, CardContent, Container } from "@mui/material";

export const BookShow = () => {
const { record } = useShowController();
return (
<Container>
<Title title={`Edit book ${record?.title}`} />
<Card>
<CardContent>
<SimpleShowLayout record={record}>
<TextField source="title" />
<TextField source="author" />
<SelectField 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" },
]} />
</SimpleShowLayout>
</CardContent>
</Card>
</Container>
);
};
```
9 changes: 3 additions & 6 deletions docs/ShowBase.md
Original file line number Diff line number Diff line change
@@ -5,15 +5,12 @@ title: "The ShowBase Component"

# `<ShowBase>`

`<ShowBase>` is a headless component that lets you build custom Show pages. It handles the logic of the Show page:

- it calls `useShowController` to fetch the record from the data provider via `dataProvider.getOne()`,
- it computes the default page title
- it creates a `ShowContext` and a `RecordContext`,
- it renders its child component
`<ShowBase>` is a headless variant of [`<Show>`](./Show.md). It fetches the record from the data provider via `dataProvider.getOne()`, puts it in a [`ShowContext`](./useShowContext.md), and renders its child. Use it to build a custom show page layout.

Contrary to [`<Show>`](./Show.md), it does not render the page layout, so no title, no actions, and no `<Card>`.

`<ShowBase>` relies on the [`useShowController`](./useShowController.md) hook.

## Usage

Use `<ShowBase>` instead of `<Show>` when you want a completely custom page layout, without the default actions and title.
32 changes: 32 additions & 0 deletions docs/SimpleForm.md
Original file line number Diff line number Diff line change
@@ -738,3 +738,35 @@ export default OrderEdit;
```

**Tip:** If you'd like to avoid creating an intermediate component like `<CityInput>`, or are using an `<ArrayInput>`, you can use the [`<FormDataConsumer>`](./Inputs.md#linking-two-inputs) component as an alternative.

## Headless Version

`<SimpleForm>` renders its children in a Material UI `<Stack>`, and renders a toolbar with a `<SaveButton>`. If you want to build a custom form layout, you can use [the `<Form>` component](./Form.md) instead.

```jsx
import { Create, Form, TextInput, RichTextInput, SaveButton } from 'react-admin';
import { Grid } from '@mui/material';

export const PostCreate = () => (
<Create>
<Form>
<Grid container>
<Grid item xs={6}>
<TextInput source="title" fullWidth />
</Grid>
<Grid item xs={6}>
<TextInput source="author" fullWidth />
</Grid>
<Grid item xs={12}>
<RichTextInput source="body" fullWidth />
</Grid>
<Grid item xs={12}>
<SaveButton />
</Grid>
</Grid>
</Form>
</Create>
);
```

React-admin forms leverage react-hook-form's [`useForm` hook](https://react-hook-form.com/docs/useform).
Binary file added docs/img/list_ant_design.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
58 changes: 33 additions & 25 deletions docs/useCreateController.md
Original file line number Diff line number Diff line change
@@ -5,44 +5,47 @@ title: "The useCreateController hook"

# `useCreateController`

The `useCreateController` hook contains the logic of [the `<Create>` component](./Create.md): it prepares a form submit handler, and returns the data and callbacks necessary to render a Creation view.
`useCreateController` contains the headless logic of the [`<Create>`](./Create.md) component. It's useful to create a custom creation view. It's also the base hook when building a custom view with another UI kit than Material UI.

React-admin calls `useCreateController` internally, when you use the `<Create>`, or `<CreateBase>` component.
`useCreateController` reads the resource name from the resource context and browser location, computes the form default values, prepares a form submit handler based on `dataProvider.create()`, computes the default page title, and returns them. Its return value matches the [`CreateContext`](./useCreateContext.md) shape.

`useCreateController` is used internally by [`<Create>`](./Create.md) and [`<CreateBase>`](./CreateBase.md). If your Create view uses react-admin components like [`<SimpleForm>`](./SimpleForm.md), prefer [`<CreateBase>`](./CreateBase.md) to `useCreateController` as it takes care of creating a `<CreateContext>`.

## Usage

Use `useCreateController` to create a custom creation view, with exactly the content you need.

{% raw %}
```jsx
import * as React from "react";
import { useCreateController, CreateContextProvider, SimpleForm, TextInput, SelectInput } from "react-admin";
import { Card } from "@mui/material";
```tsx
import { useCreateController, SelectInput, SimpleForm, TextInput, Title } from "react-admin";
import { Card, CardContent, Container } from "@mui/material";

export const BookCreate = () => {
const { save } = useCreateController({ resource: 'books' });
return (
<div>
<Title title="Book Creation" />
<Card>
<SimpleForm onSubmit={save} record={{}} >
<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>
</Card>
</div>
);
const { save } = useCreateController();
return (
<Container>
<Title title="Create book" />
<Card>
<CardContent>
<SimpleForm onSubmit={save}>
<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>
</Container>
);
};
```
{% endraw %}

**Tip**: If you just use the return value of `useCreateController` to put it in an `CreateContext`, use [the `<CreateBase>` component](./CreateBase.md) instead for simpler markup.

## Input Format

`useCreateController` accepts an options argument, with the following fields, all optional:

* [`disableAuthentication`](./Create.md#disableauthentication): disable the authentication check
@@ -52,11 +55,16 @@ export const BookCreate = () => {
* [`resource`](./Create.md#resource): override the name of the resource to create
* [`transform`](./Create.md#transform): transform the form data before calling `dataProvider.create()`

These fields are documented in [the `<Create>` component](./Create.md) documentation.

## Return Value

`useCreateController` returns an object with the following fields:

```jsx
const {
defaultTitle, // the translated title based on the resource, e.g. 'Create New Post'
record, // the default values of the creation form
redirect, // the default redirection route. Defaults to 'list'
resource, // the resource name, deduced from the location. e.g. 'posts'
save, // the update callback, to be passed to the underlying form as submit handler
15 changes: 10 additions & 5 deletions docs/useEditController.md
Original file line number Diff line number Diff line change
@@ -5,17 +5,17 @@ title: "The useEditController hook"

# `useEditController`

The `useEditController` hook contains the logic of [the `<Edit>` component](./Edit.md): it fetches a record based on the URL, prepares a form submit handler, and returns all the data and callbacks necessary to render an edition view.
`useEditController` contains the headless logic of the [`<Edit>`](./Edit.md) component. It's useful to create a custom edition view. It's also the base hook when building a custom view with another UI kit than Material UI.

React-admin calls `useEditController` internally when you use the `<Edit>`, `<EditBase>`, or `<EditGuesser>` component.
`useEditController` reads the resource name and id from the resource context and browser location, fetches the record via `dataProvider.getOne()` to initialize the form, prepares a form submit handler based on `dataProvider.update()`, computes the default page title, and returns them. Its return value matches the [`EditContext`](./useEditContext.md) shape.

`useEditController` is used internally by [`<Edit>`](./Edit.md) and [`<EditBase>`](./EditBase.md). If your Edit view uses react-admin components like [`<SimpleForm>`](./SimpleForm.md), prefer [`<EditBase>`](./EditBase.md) to `useEditController` as it takes care of creating a `<EditContext>`.

## Usage

Use `useEditController` to create a custom Edition view, with exactly the content you need.

{% raw %}
```jsx
import * as React from "react";
import { useParams } from "react-router-dom";
import { useEditController, EditContextProvider, SimpleForm, TextInput, SelectInput } from "react-admin";
import { Card } from "@mui/material";
@@ -42,10 +42,11 @@ export const BookEdit = () => {
);
};
```
{% endraw %}

**Tip**: If you just use the return value of `useEditController` to put it in an `EditContext`, use [the `<EditBase>` component](./EditBase.md) instead for simpler markup.

## Input Format

`useEditController` accepts an options argument, with the following fields, all optional:

* [`disableAuthentication`](./Edit.md#disableauthentication): disable the authentication check
@@ -57,6 +58,10 @@ export const BookEdit = () => {
* [`resource`](./Edit.md#resource): override the name of the resource to create
* [`transform`](./Edit.md#transform): transform the form data before calling `dataProvider.update()`

These fields are documented in [the `<Edit>` component](./Edit.md) documentation.

## Return Value

`useEditController` returns an object with the following fields:

```jsx
82 changes: 79 additions & 3 deletions docs/useListController.md
Original file line number Diff line number Diff line change
@@ -5,13 +5,89 @@ title: "useListController"

# `useListController`

The `useListController` hook fetches the data, prepares callbacks for modifying the pagination, filters, sort and selection, and returns them. Its return value match the `ListContext` shape. `useListController` is used internally by the `<List>` and `<ListBase>` components.
`useListController` contains the headless logic of the [`<List>`](./List.md) component. It's useful to create a custom List view. It's also the base hook when building a custom view with another UI kit than Material UI.

You can use it to create a custom List view, although its component counterpart, [`<ListBase>`](./ListBase.md), is probably better in most cases.
![List view built with Ant Design](./img/list_ant_design.png)

`useListController` reads the list parameters from the URL, calls `dataProvider.getList()`, prepares callbacks for modifying the pagination, filters, sort and selection, and returns them together with the data. Its return value matches the [`ListContext`](./useListContext.md) shape.

`useListController` is used internally by [`<List>`](./List.md) and [`<ListBase>`](./ListBase.md). If your list view uses react-admin components like [`<Datagrid>`](./Datagrid.md), prefer [`<ListBase>`](./ListBase.md) to `useListController` as it takes care of creating a `<ListContext>`.

## Usage

It's common to call `useListController()` without parameters, and to put the result in a `ListContext` to make it available to the rest of the component tree.
`useListController` expects a parameters object defining the list sorting, pagination, and filters. It returns an object with the fetched data, and callbacks to modify the list parameters.

Here the code for the post list view above, built with [Ant Design](https://ant.design/):

{% raw %}
```jsx
import { useListController } from 'react-admin';
import { Card, Table, Button } from 'antd';
import {
CheckCircleOutlined,
PlusOutlined,
EditOutlined,
} from '@ant-design/icons';
import { Link } from 'react-router-dom';

const PostList = () => {
const { data, page, total, setPage, isLoading } = useListController({
sort: { field: 'published_at', order: 'DESC' },
perPage: 10,
});
const handleTableChange = (pagination) => {
setPage(pagination.current);
};
return (
<>
<div style={{ margin: 10, textAlign: 'right' }}>
<Link to="/posts/create">
<Button icon={<PlusOutlined />}>Create</Button>
</Link>
</div>
<Card bodyStyle={{ padding: '0' }} loading={isLoading}>
<Table
size="small"
dataSource={data}
columns={columns}
pagination={{ current: page, pageSize: 10, total }}
onChange={handleTableChange}
/>
</Card>
</>
);
};

const columns = [
{ title: 'Id', dataIndex: 'id', key: 'id' },
{ title: 'Title', dataIndex: 'title', key: 'title' },
{
title: 'Publication date',
dataIndex: 'published_at',
key: 'pub_at',
render: (value) => new Date(value).toLocaleDateString(),
},
{
title: 'Commentable',
dataIndex: 'commentable',
key: 'commentable',
render: (value) => (value ? <CheckCircleOutlined /> : null),
},
{
title: 'Actions',
render: (_, record) => (
<Link to={`/posts/${record.id}`}>
<Button icon={<EditOutlined />}>Edit</Button>
</Link>
),
},
];

export default PostList;
```
{% endraw %}

When using react-admin components, it's common to call `useListController()` without parameters, and to put the result in a `ListContext` to make it available to the rest of the component tree.

```jsx
import {
8 changes: 3 additions & 5 deletions docs/useShowController.md
Original file line number Diff line number Diff line change
@@ -5,13 +5,11 @@ title: "useShowController"

# `useShowController`

`useShowController` is the hook that handles all the controller logic for Show views. It's used by [`<Show>`](./Show.md) and [`<ShowBase>`](./ShowBase.md).
`useShowController` contains the headless logic of the [`<Show>`](./Show.md) component. It's useful to create a custom Show view. It's also the base hook when building a custom view with another UI kit than Material UI.

This hook takes care of three things:
`useShowController` reads the resource name and id from the resource context and browser location, fetches the record from the data provider via `dataProvider.getOne()`, computes the default page title, and returns them. Its return value matches the [`ShowContext`](./useShowContext.md) shape.

- it reads the resource name and id from the resource context and browser location
- it fetches the record from the data provider via `dataProvider.getOne()`,
- it computes the default page title
`useShowController` is used internally by [`<Show>`](./Show.md) and [`<ShowBase>`](./ShowBase.md). If your Show view uses react-admin components like `<TextField>`, prefer [`<ShowBase>`](./ShowBase.md) to `useShowController` as it takes care of creating a `<ShowContext>`.

## Usage

24 changes: 22 additions & 2 deletions packages/ra-ui-materialui/src/form/SimpleForm.stories.tsx
Original file line number Diff line number Diff line change
@@ -5,12 +5,13 @@ import {
ResourceContextProvider,
testDataProvider,
} from 'ra-core';
import { Stack, ThemeProvider, createTheme } from '@mui/material';
import { MemoryRouter } from 'react-router-dom';

import { AdminContext } from '../AdminContext';
import { Edit } from '../detail';
import { NumberInput, TextInput } from '../input';
import { SimpleForm } from './SimpleForm';
import { Stack } from '@mui/material';

export default { title: 'ra-ui-materialui/forms/SimpleForm' };

@@ -33,7 +34,7 @@ const Wrapper = ({
i18nProvider={i18nProvider}
dataProvider={testDataProvider({
getOne: () => Promise.resolve({ data }),
})}
} as any)}
>
<ResourceContextProvider value="books">
<Edit id={1} sx={{ width: 600 }}>
@@ -169,3 +170,22 @@ export const InputBasedValidation = () => (
</SimpleForm>
</Wrapper>
);

export const Controlled = () => {
const [record, setRecord] = React.useState({} as any);
return (
<MemoryRouter>
<ThemeProvider theme={createTheme()}>
<SimpleForm
resource="books"
onSubmit={values => setRecord(values)}
>
<TextInput source="title" fullWidth />
<TextInput source="author" />
<NumberInput source="year" />
</SimpleForm>
<div>Record values: {JSON.stringify(record)}</div>
</ThemeProvider>
</MemoryRouter>
);
};

0 comments on commit d1042fd

Please sign in to comment.