diff --git a/docs/RecordField.md b/docs/RecordField.md new file mode 100644 index 00000000000..344a900c852 --- /dev/null +++ b/docs/RecordField.md @@ -0,0 +1,411 @@ +--- +layout: default +title: "The RecordField Component" +--- + +# `` + +`` displays a label and a record property. + +![RecordField](./img/RecordField.png) + +## Usage + +Use `` as descendent of a [`RecordContextProvider`](./useRecordContext.md#creating-a-record-context) like in record detail components (``, ``, ``, ``). + +For instance, to render the title of a book in a show view: + +```jsx +import { Show, RecordField } from 'react-admin'; +import { Stack } from '@mui/material'; + +export const BookShow = () => ( + + + + + +); +``` + +`` renders a label based on the humanized `source` prop, or on the `label` prop if present. It also grabs the `record` from the current [`RecordContext`](./useRecordContext.md), extracts the `record[source]` property, and displays it using a [``](./TextField.md) by default. + +You can override the label by passing a `label` prop: + +```jsx + +``` + +The `source` prop can be a [deep source](./Fields.md#deep-field-source): + +```jsx + +``` + +You can customize the way the value is displayed by passing a Field component in the `field` prop. For example, to display a numeric value using the browser locale, use the `NumberField`: + +```jsx +import { RecordField, NumberField } from 'react-admin'; + + +``` + +If you need to pass specific props to the field component, for example to format the value, prefer passing a field component as child. In this case, the `source` passed to the `RecordField` will only be used for the label: + +{% raw %} +```jsx +import { RecordField, NumberField } from 'react-admin'; + + + + +``` +{% endraw %} + +If you need to aggregate multiple fields, you can use the `render` prop instead, to pass a function that receives the current record and returns a React element: + +```jsx +import { RecordField } from 'react-admin'; + + `${record.firstName} ${record.lastName}`} +/> +``` + +The `source`, `field`, `children`, and `render` props are mutually exclusive. + +## Props + +| Prop | Required | Type | Default | Description | +| ----------- | -------- | ----------------------- | ------- | -------------------------------------------------------------------------------- | +| `children` | Optional | ReactNode | '' | Elements rendering the actual field. | +| `className` | Optional | string | '' | CSS class name to apply to the field. | +| `empty` | Optional | ReactNode | '' | Text to display when the field is empty. | +| `field` | Optional | ReactElement | `TextField` | Field component used to render the field. Ignored if `children` or `render` are set. | +| `label` | Optional | string | '' | Label to render. Can be a translation key. | +| `record` | Optional | object | {} | Record to use. If not set, the record is taken from the context. | +| `render` | Optional | record => JSX | | Function to render the field value. Ignored if `children` is set. | +| `source` | Optional | string | '' | Name of the record field to render. | +| `sx` | Optional | object | {} | Styles to apply to the field. | +| `TypographyProps` | Optional | object | {} | Props to pass to label wrapper | +| `variant` | Optional | `'default' || 'inline'` | 'default' | When `inline`, the label is displayed inline with the field value. | + +## `children` + +The `children` prop is used to pass a field component that will be rendered instead of the default one. The `source` prop will only be used for the label. + +{% raw %} +```jsx +import { RecordField, NumberField } from 'react-admin'; + + + + +``` +{% endraw %} + +This ability is often used to render a field from a reference record, using [``](./ReferenceField.md): + +```jsx +import { RecordField, ReferenceField } from 'react-admin'; + + + + +``` + +If you just need to use a field component without any special prop, prefer the `field` prop: + +```jsx +import { RecordField, NumberField } from 'react-admin'; + + +// instead of + + + +``` + +## `empty` + +When the record contains no value for the `source` prop, `RecordField` renders an empty string. If you need to render a custom string in this case, you can use the `empty` prop : + +```jsx + +``` + +`empty` also accepts a translation key, so you can have a localized string when the field is empty: + +```jsx + +``` + +If you use the `render` prop, you can even use a React element as `empty` value. + +{% raw %} +```jsx +Missing title} + render={record => record.title} +/> +``` +{% endraw %} + +Note that `empty` is ignored when you pass a custom field component as child. In this case, it's the child's responsibility to handle the empty value. + +```jsx + + + +``` + +## `field` + +By default, `` uses the [``](./TextField.md) component to render the field value. + +```jsx + +// equivalent to + +``` + +Use the `field` prop to pass a custom field component instead: + +```jsx +import { RecordField, NumberField } from 'react-admin'; + + +``` + +If you need to pass specific props to the field component, for example to format the value, prefer passing a field component as child. In this case, the `source` passed to the `RecordField` will only be used for the label: + +{% raw %} +```jsx +import { RecordField, NumberField } from 'react-admin'; + + + + +``` +{% endraw %} + +## `label` + +When you use the `source` prop, the label is automatically generated from the source name using the "humanize" function. For example, the source `author.name` will be displayed as "Author name". + +You can customize the label by passing a custom [translation](./Translation.md) for the `resources.${resourceName}.fields.${source}` key. For example, if you have a resource called `posts`, and you want to customize the label for `` field, you can add the following translation: + +```json +{ + "resources": { + "posts": { + "fields": { + "title": "Post title" + } + } + } +} +``` + +If you don't use the `source` prop, or if you don't want to use the i18N features to customize the label, you can use the `label` prop to override the default label: + +```jsx + +``` + +If you pass a translation key as `label`, react-admin will use the `i18nProvider` to translate it: + +```jsx + +``` + +Finally, you can pass `false` to the `label` prop to hide the label: + +```jsx + +``` + +Note that using `label={false}` is equivalent to rendering a `` directly. + +## `record` + +By default, `` uses the record from the current [`RecordContext`](./useRecordContext.md). You can override this behavior by passing a `record` prop: + +```jsx + +``` + +## `render` + +The `render` prop is used to pass a function that receives the current record and returns a React element. This is useful when you need to aggregate multiple fields, or when you need to use a component that doesn't accept the `source` prop. + +```jsx +import { RecordField } from 'react-admin'; + + `${record.firstName} ${record.lastName}`} +/> +``` + +If you pass both `source` and `render`, the `source` will be used for the label only. + +## `sx` + +Use the `sx` prop to pass custom styles to the field. + +{% raw %} +```jsx + +``` +{% endraw %} + +If you want to style the label, use the `TypographyProps` prop instead: + +{% raw %} +```jsx + +``` +{% endraw %} + +If you want to style the value only, prefer passing a custom component as child: + +{% raw %} +```jsx + + + +``` +{% endraw %} + +## `source` + +Use the `source` prop to specify the name of the record field to render. + +For example, if the current record is: + +```json +{ + "id": 123, + "title": "My post", + "author": { + "name": "John Doe" + } +} +``` + +To display the `title` field, use: + +```jsx + +``` + +The `source` prop can be a deep source, for example `author.name`. + +```jsx + +``` + +If you use the `render` or `children` prop, the `source` will only be used for the label. + +## `TypographyProps` + +The `TypographyProps` prop is used to pass props to the label wrapper. This is useful when you want to style the label differently from the field value. + +{% raw %} +```jsx + +``` +{% endraw %} + +## `variant` + +By default, `` renders the label above the field value. You can use the `variant` prop to render the label inline with the field value: + +```jsx + +``` + +If you need to customize the width of the label, you can use the `TypographyProps` prop: + +{% raw %} +```jsx + +``` +{% endraw %} + +But since you generally need to do it for several fields, it's preferable to do it in the parent component: + +{% raw %} +```jsx + + + + + + + +``` +{% endraw %} + +**Tip**: If you want all your fields to be displayed inline, you can define the default variant for `RecordField` [in a custom application Theme](https://marmelab.com/react-admin/AppTheme.html#theming-individual-components): + +```jsx +import { defaultTheme } from 'react-admin'; +import { deepmerge } from '@mui/utils'; + +const theme = deepmerge(defaultTheme, { + components: { + RaRecordField: { + defaultProps: { + variant: 'inline', + }, + }, + }, +}); + +const App = () => ( + + // ... + +); +``` + +## TypeScript + +`` is a generic component. You can pass a type parameter to get hints for the `source` prop and type safety for the `record` argument of the `render` function. + +```tsx +import { Show, RecordField } from 'react-admin'; +import { Stack } from '@mui/material'; + +import { Book } from './types'; + +const BookShow = () => { + const BookField = RecordField; + return ( + + + + + `${record.price} USD`} /> + + + ); +}; +``` diff --git a/docs/Reference.md b/docs/Reference.md index 8b088524b43..257c9460c1f 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -149,6 +149,7 @@ title: "Index" **- R -** * [``](./RadioButtonGroupInput.md) +* [``](./RecordField.md) * [``](./RecordRepresentation.md) * [``](./ReferenceArrayField.md) * [``](./ReferenceArrayInput.md) diff --git a/docs/_includes/navigation.html b/docs/_includes/navigation.html index 881716d29f2..e17da0a23a1 100644 --- a/docs/_includes/navigation.html +++ b/docs/_includes/navigation.html @@ -183,6 +183,7 @@
  • <ImageField>
  • <MarkdownField>
  • <NumberField>
  • +
  • <RecordField>
  • <ReferenceField>
  • <ReferenceArrayField>
  • <ReferenceManyField>
  • diff --git a/docs/img/RecordField.png b/docs/img/RecordField.png new file mode 100644 index 00000000000..450cb8a038b Binary files /dev/null and b/docs/img/RecordField.png differ diff --git a/examples/simple/src/comments/CommentShow.tsx b/examples/simple/src/comments/CommentShow.tsx index 50a33b8c8db..8a84baf2d05 100644 --- a/examples/simple/src/comments/CommentShow.tsx +++ b/examples/simple/src/comments/CommentShow.tsx @@ -1,23 +1,18 @@ import * as React from 'react'; -import { - DateField, - ReferenceField, - Show, - SimpleShowLayout, - TextField, -} from 'react-admin'; +import { DateField, ReferenceField, RecordField, Show } from 'react-admin'; +import { Stack } from '@mui/material'; const CommentShow = () => ( - - - - - - - - - + + + + + + + + + ); diff --git a/packages/ra-ui-materialui/src/field/RecordField.spec.tsx b/packages/ra-ui-materialui/src/field/RecordField.spec.tsx new file mode 100644 index 00000000000..ce4374e713c --- /dev/null +++ b/packages/ra-ui-materialui/src/field/RecordField.spec.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import expect from 'expect'; +import { render, screen } from '@testing-library/react'; + +import { + Basic, + Source, + Label, + Empty, + Render, + Field, + Children, +} from './RecordField.stories'; +export default { + title: 'ra-ui-materialui/fields/RecordField', +}; + +describe('', () => { + describe('source', () => { + it('should render the source field from the record in context', () => { + render(); + expect(screen.queryByText('War and Peace')).not.toBeNull(); + }); + it('should render nothing when the source is not found', () => { + render(); + expect(screen.queryByText('Missing field')).not.toBeNull(); + }); + it('should support paths with dots', () => { + render(); + expect(screen.queryByText('Leo Tolstoy')).not.toBeNull(); + }); + }); + describe('label', () => { + it('should render the humanized source as label by default', () => { + render(); + expect(screen.queryByText('Title')).not.toBeNull(); + }); + it('should render the label prop as label', () => { + render(