Skip to content

Commit

Permalink
Merge pull request #4116 from marmelab/demo-customer
Browse files Browse the repository at this point in the history
Add the ability to override form layouts in a simpler way
  • Loading branch information
djhi authored Dec 9, 2019
2 parents 2f9a703 + dd62ae0 commit 07b5e4c
Show file tree
Hide file tree
Showing 23 changed files with 1,524 additions and 761 deletions.
160 changes: 160 additions & 0 deletions docs/CreateEdit.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Here are all the props accepted by the `<Create>` and `<Edit>` components:
* [`actions`](#actions)
* [`aside`](#aside-component)
* [`successMessage`](#success-message)
* [`component](#component)
* [`undoable`](#undoable) (`<Edit>` only)

Here is the minimal code necessary to display a form to create and edit comments:
Expand Down Expand Up @@ -192,6 +193,30 @@ const PostEdit = props => (
**Tip**: The message will be translated.
### Component
By default, the Create and Edit views render the main form inside a material-ui `<Card>` element. The actual layout of the form depends on the `Form` component you're using (`<SimpleForm>`, `<TabbedForm>`, or a custom form component).
Some form layouts also use `Card`, in which case the user ends up seeing a card inside a card, which is bad UI. To avoid that, you can override the main form container by passing a `component` prop :
```jsx
// use a div as root component
const PostEdit = props => (
<Edit component="div" {...props}>
...
</Edit>
);

// use a custom component as root component
const PostEdit = props => (
<Edit component={MyComponent} {...props}>
...
</Edit>
);
```
The default value for the `component` prop is `Card`.
### Undoable
By default, the Save and Delete actions are undoable, i.e. react-admin only sends the related request to the data provider after a short delay, during which the user can cancel the action. This is part of the "optimistic rendering" strategy of react-admin ; it makes the user interactions more reactive.
Expand Down Expand Up @@ -928,6 +953,141 @@ export const UserEdit = props => {
}
```

## Customizing The Form Layout

The `<SimpleForm>` and `<TabbedForm>` layouts are quite simple. In order to better use the screen real estate, you may want to arrange inputs differently, e.g. putting them in groups, adding separators, etc. For that purpose, you need to write a custom form layout, and use it instead of `<SimpleForm>`.

![custom form layout](./img/custom-form-layout.png)

Here is an example of such custom form, taken from the Posters Galore demo. It uses [material-ui's `<Box>` component](https://material-ui.com/components/box/), and it's a good starting point for your custom form layouts.

```jsx
import {
FormWithRedirect,
DateInput,
SelectArrayInput,
TextInput,
Toolbar,
SaveButton,
DeleteButton,
} from 'react-admin';
import { CardContent, Typography, Box, Toolbar } from '@material-ui/core';
const VisitorForm = (props) => (
<FormWithRedirect
{...props}
render={formProps => (
// here starts the custom form layout
<form>
<Box p="1em">
<Box display="flex">
<Box flex={2} mr="1em">
<Typography variant="h6" gutterBottom>Identity</Typography>
<Box display="flex">
<Box flex={1} mr="0.5em">
<TextInput source="first_name" resource="customers" fullWidth />
</Box>
<Box flex={1} ml="0.5em">
<TextInput source="last_name" resource="customers" fullWidth />
</Box>
</Box>
<TextInput source="email" resource="customers" type="email" fullWidth />
<DateInput source="birthday" resource="customers" />
<Box mt="1em" />
<Typography variant="h6" gutterBottom>Address</Typography>
<TextInput resource="customers" source="address" multiline fullWidth />
<Box display="flex">
<Box flex={1} mr="0.5em">
<TextInput source="zipcode" resource="customers" fullWidth />
</Box>
<Box flex={2} ml="0.5em">
<TextInput source="city" resource="customers" fullWidth />
</Box>
</Box>
</Box>
<Box flex={1} ml="1em">
<Typography variant="h6" gutterBottom>Stats</Typography>
<SelectArrayInput source="groups" resource="customers" choices={segments} fullWidth />
<NullableBooleanInput source="has_newsletter" resource="customers" />
</Box>
</Box>
</Box>
<Toolbar>
<Box display="flex" justifyContent="space-between" width="100%">
<SaveButton
saving={formProps.saving}
handleSubmitSithRedirect={formProps.handleSubmitSithRedirect}
/>
<DeleteButton record={formProps.record} />
</Box>
</Toolbar>
</form>
)}
/>
);
```

This custom form layout component uses the `FormWithRedirect` component, which wraps react-final-form's `Form` component to handle redirection logic. It also uses react-admin's `<SaveButton>` and a `<DeleteButton>`.

**Tip**: When `Input` components have a `resource` prop, they use it to determine the input label. `<SimpleForm>` and `<TabbedForm>` inject this `resource` prop to `Input` components automatically. When you use a custom form layout, pass the `resource` prop manually - unless the `Input` has a `label` prop.

To use this form layout, simply pass it as child to an `Edit` component:

```jsx
const VisitorEdit = props => (
<Edit {...props}>
<VisitorForm />
</Edit>
);
```

**Tip**: `FormWithRedirect` contains some logic that you may not want. In fact, nothing forbids you from using [a react-final-form `Form` component](https://final-form.org/docs/react-final-form/api/Form) as root component for a custom form layout. You'll have to set initial values based the injected `record` prop manually, as follows:

{% raw %}
```jsx
import { sanitizeEmptyValues } from 'react-admin';
import { Form } from 'react-final-form';
import arrayMutators from 'final-form-arrays';
import { CardContent, Typography, Box } from '@material-ui/core';
// the parent component (Edit or Create) injects these props to their child
const VisitorForm = ({ basePath, record, save, saving, version }) => {
const submit = values => {
// React-final-form removes empty values from the form state.
// To allow users to *delete* values, this must be taken into account
save(sanitizeEmptyValues(record, values));
};
return (
<Form
initialValues={record}
onSubmit={submit}
mutators={{ ...arrayMutators }} // necessary for ArrayInput
subscription={defaultSubscription} // don't redraw entire form each time one field changes
key={version} // support for refresh button
keepDirtyOnReinitialize
render={formProps => (
// render your custom form here
)}
/>
);
};
const defaultSubscription = {
submitting: true,
pristine: true,
valid: true,
invalid: true,
};
```
{% endraw %}

## Displaying Fields or Inputs depending on the user permissions

You might want to display some fields, inputs or filters only to users with specific permissions.
Expand Down
24 changes: 24 additions & 0 deletions docs/List.md
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,30 @@ const Aside = ({ data, ids }) => (
```
{% endraw %}
### Component
By default, the List view renders the main content area inside a material-ui `<Card>` element. The actual layout of the list depends on the child component you're using (`<Datagrid>`, `<SimpleList>`, or a custom layout component).
Some layouts also use `Card`, in which case the user ends up seeing a card inside a card, which is bad UI. To avoid that, you can override the main area container by passing a `component` prop:
```jsx
// use a div as root component
const PostList = props => (
<List component="div" {...props}>
...
</List>
);

// use a custom component as root component
const PostList = props => (
<List component={MyComponent} {...props}>
...
</List>
);
```
The default value for the `component` prop is `Card`.
### CSS API
The `List` component accepts the usual `className` prop but you can override many class names injected to the inner components by React-admin thanks to the `classes` property (as most Material UI components, see their [documentation about it](https://material-ui.com/customization/components/#overriding-styles-with-classes)). This property accepts the following keys:
Expand Down
25 changes: 25 additions & 0 deletions docs/Show.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Here are all the props accepted by the `<Show>` component:
* [`title`](#page-title)
* [`actions`](#actions)
* [`aside`](#aside-component)
* [`component`](#component)

Here is the minimal code necessary to display a view to show a post:

Expand Down Expand Up @@ -151,6 +152,30 @@ const Aside = ({ record }) => (

**Tip**: Always test that the `record` is defined before using it, as react-admin starts rendering the UI before the API call is over.

### Component

By default, the Show view renders the main content area inside a material-ui `<Card>` element. The actual layout of the area depends on the `ShowLayout` component you're using (`<SimpleShowLayout>`, `<TabbedShowLayout>`, or a custom layout component).

Some layouts also use `Card`, in which case the user ends up seeing a card inside a card, which is bad UI. To avoid that, you can override the main area container by passing a `component` prop:

```jsx
// use a div as root component
const PostShow = props => (
<Show component="div" {...props}>
...
</Show>
);

// use a custom component as root component
const PostShow = props => (
<Show component={MyComponent} {...props}>
...
</Show>
);
```

The default value for the `component` prop is `Card`.

## The `<ShowGuesser>` component

Instead of a custom `Show`, you can use the `ShowGuesser` to determine which fields to use based on the data returned by the API.
Expand Down
Binary file added docs/img/custom-form-layout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 7 additions & 3 deletions examples/demo/src/i18n/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,26 @@ export default {
name: 'Customer |||| Customers',
fields: {
commands: 'Orders',
first_seen: 'First seen',
groups: 'Segments',
last_seen: 'Last seen',
last_seen_gte: 'Visited Since',
name: 'Name',
total_spent: 'Total spent',
},
tabs: {
fieldGroups: {
identity: 'Identity',
address: 'Address',
orders: 'Orders',
reviews: 'Reviews',
stats: 'Stats',
history: 'History',
},
page: {
delete: 'Delete Customer',
},
},
commands: {
name: 'Order |||| Orders',
amount: '1 order |||| %{smart_count} orders',
title: 'Order %{reference}',
fields: {
basket: {
Expand Down Expand Up @@ -121,6 +123,8 @@ export default {
},
reviews: {
name: 'Review |||| Reviews',
amount: '1 review |||| %{smart_count} reviews',
relative_to_poster: 'Review on poster',
detail: 'Review detail',
fields: {
customer_id: 'Customer',
Expand Down
8 changes: 5 additions & 3 deletions examples/demo/src/i18n/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,19 @@ export default {
total_spent: 'Dépenses',
zipcode: 'Code postal',
},
tabs: {
fieldGroups: {
identity: 'Identité',
address: 'Adresse',
orders: 'Commandes',
reviews: 'Commentaires',
stats: 'Statistiques',
history: 'Historique',
},
page: {
delete: 'Supprimer le client',
},
},
commands: {
name: 'Commande |||| Commandes',
amount: '1 commande |||| %{smart_count} commandes',
title: 'Commande n°%{reference}',
fields: {
basket: {
Expand Down Expand Up @@ -138,6 +138,8 @@ export default {
},
reviews: {
name: 'Commentaire |||| Commentaires',
amount: '1 commentaire |||| %{smart_count} commentaires',
relative_to_poster: 'Commentaire sur',
detail: 'Détail du commentaire',
fields: {
customer_id: 'Client',
Expand Down
14 changes: 12 additions & 2 deletions examples/demo/src/reviews/ReviewListDesktop.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ const useListStyles = makeStyles({
borderLeftWidth: 5,
borderLeftStyle: 'solid',
},
headerCell: {
padding: '6px 8px 6px 8px',
},
rowCell: {
padding: '6px 8px 6px 8px',
},
comment: {
maxWidth: '18em',
overflow: 'hidden',
Expand All @@ -28,14 +34,18 @@ const ReviewListDesktop = props => {
<Datagrid
rowClick="edit"
rowStyle={rowStyle}
classes={{ headerRow: classes.headerRow }}
classes={{
headerRow: classes.headerRow,
headerCell: classes.headerCell,
rowCell: classes.rowCell,
}}
optimized
{...props}
>
<DateField source="date" />
<CustomerReferenceField link={false} />
<ProductReferenceField link={false} />
<StarRatingField />
<StarRatingField size="small" />
<TextField source="comment" cellClassName={classes.comment} />
<TextField source="status" />
</Datagrid>
Expand Down
Loading

0 comments on commit 07b5e4c

Please sign in to comment.