Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/ArrayField.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,13 @@ Check [the `useListContext` documentation](./useListContext.md) for more informa

## Rendering An Array Of Strings

If you need to render a custom collection (e.g. an array of tags `['dolor', 'sit', 'amet']`), it's often simpler to write your own component:
If you need to render a custom collection (e.g. an array of tags `['dolor', 'sit', 'amet']`), you can use the [`<TextArrayField>`](./TextArrayField.md) component.

```jsx
<TextArrayField source="tags" />
```

You can also create your own field component, using the `useRecordContext` hook:

```jsx
import { useRecordContext } from 'react-admin';
Expand Down
10 changes: 10 additions & 0 deletions docs/ChipField.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,13 @@ The `<ChipField>` component accepts the usual `className` prop. You can also ove
| `&.RaChipField-chip` | Applied to the underlying Material UI's `Chip` component |

To override the style of all instances of `<ChipField>` using the [application-wide style overrides](./AppTheme.md#theming-individual-components), use the `RaChipField` key.

## Rendering A Scalar Value

If you need to render a custom collection (e.g. an array of tags `['dolor', 'sit', 'amet']`), you may be tempted to use `<ChipField source="." />`, but that won't work.

What you probably need in that case instead is the [`<TextArrayField>`](./TextArrayField.md) component, which will render each item of a scalar array in its own Chip.

```jsx
<TextArrayField source="tags" />
```
1 change: 1 addition & 0 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ title: "Index"
* [`<TabbedForm>`](./TabbedForm.md)
* [`<TabbedFormWithRevision>`](./TabbedForm.md#versioning)<img class="icon" src="./img/premium.svg" alt="React Admin Enterprise Edition icon" />
* [`<TabbedShowLayout>`](./TabbedShowLayout.md)
* [`<TextArrayField>`](./TextArrayField.md)
* [`<TextArrayInput>`](./TextArrayInput.md)
* [`<TextField>`](./TextField.md)
* [`<TextInput>`](./TextInput.md)
Expand Down
8 changes: 8 additions & 0 deletions docs/SingleFieldList.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,11 @@ The `<SingleFieldList>` component accepts the usual `className` prop. You can al
| `& .RaSingleFieldList-link` | Applied to each link |

**Tip**: You can override these classes for all `<SingleFieldList>` instances by overriding them in a Material UI theme, using the key "RaSingleFieldList".

## Rendering An Array Of Strings

If you need to render a custom collection (e.g. an array of tags `['dolor', 'sit', 'amet']`), you may want to use the [`<TextArrayField>`](./TextArrayField.md) component instead.

```jsx
<TextArrayField source="tags" />
```
138 changes: 138 additions & 0 deletions docs/TextArrayField.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
layout: default
title: "The TextArrayField Component"
storybook_path: ra-ui-materialui-fields-textarrayfield--basic
---

# `<TextArrayField>`

`<TextArrayField>` renders an array of scalar values using Material-UI's Stack and Chips.

![TextArrayField](./img/text-array-field.png)

`<TextArrayField>` is ideal for displaying lists of simple text values, such as genres or tags, in a visually appealing way.

## Usage

`<TextArrayField>` can be used in a Show view to display an array of values from a record. For example:

```js
const book = {
id: 1,
title: 'War and Peace',
genres: [
'Fiction',
'Historical Fiction',
'Classic Literature',
'Russian Literature',
],
};
```

You can render the `TextArrayField` like this:

```jsx
import { Show, SimpleShowLayout, TextArrayField } from 'react-admin';

const BookShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="title" />
<TextArrayField source="genres" />
</SimpleShowLayout>
</Show>
);
```

## Props

The following props are available for `<TextArrayField>`:

| Prop | Type | Required | Description |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: the Required column is the second one in all prop tables, here it's the third. Also, we usually include a Default column.

Copy link
Contributor Author

@slax57 slax57 Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay I'll change that on master
UPDATE: I mean on next

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: d785636

| ----------- | ------------ | -------- | ------------------------------------------------------------- |
| `source` | `string` | Yes | The name of the record field containing the array to display. |
| `color` | `string` | - | The color of the Chip components. |
| `emptyText` | `ReactNode` | - | Text to display when the array is empty. |
| `record` | `RecordType` | - | The record containing the data to display. |
| `size` | `string` | - | The size of the Chip components. |
| `variant` | `string` | - | The variant of the Chip components. |

Additional props are passed to the underlying [Material-UI `Stack` component](https://mui.com/material-ui/react-stack/).

## `color`

The color of the Chip components. Accepts any value supported by MUI's Chip (`primary`, `secondary`, etc).

```jsx
<TextArrayField source="genres" color="secondary" />
```

## `direction`

The direction of the Stack layout. Accepts `row` or `column`. The default is `row`.

```jsx
<TextArrayField source="genres" direction="column" />
```

## `emptyText`

Text to display when the array is empty.

```jsx
<TextArrayField source="genres" emptyText="No genres available" />
```

## `record`

The record containing the data to display. Usually provided by react-admin automatically.

```jsx
const book = {
id: 1,
title: 'War and Peace',
genres: [
'Fiction',
'Historical Fiction',
'Classic Literature',
'Russian Literature',
],
};

<TextArrayField source="genres" record={book} />
```

## `size`

The size of the Chip components. Accepts any value supported by MUI's Chip (`small`, `medium`). The default is `small`.

```jsx
<TextArrayField source="genres" size="medium" />
```

## `source`

The name of the record field containing the array to display.

```jsx
<TextArrayField source="genres" />
```

## `sx`

Custom styles for the Stack, using MUI's `sx` prop.

{% raw %}
```jsx
<TextArrayField source="genres" sx={{ gap: 2 }} />
```
{% endraw %}

## `variant`

The variant of the Chip components. Accepts any value supported by MUI's Chip (`filled`, `outlined`). The default is `filled`.

```jsx
<TextArrayField source="genres" variant="outlined" />
```

Binary file added docs/img/text-array-field.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@
<li {% if page.path == 'ReferenceOneField.md' %} class="active" {% endif %}><a class="nav-link" href="./ReferenceOneField.html"><code>&lt;ReferenceOneField&gt;</code></a></li>
<li {% if page.path == 'RichTextField.md' %} class="active beginner" {% else %} class="beginner" {% endif %}><a class="nav-link" href="./RichTextField.html"><code>&lt;RichTextField&gt;</code></a></li>
<li {% if page.path == 'SelectField.md' %} class="active beginner" {% else %} class="beginner" {% endif %}><a class="nav-link" href="./SelectField.html"><code>&lt;SelectField&gt;</code></a></li>
<li {% if page.path == 'TextArrayField.md' %} class="active beginner" {% else %} class="beginner" {% endif %}><a class="nav-link" href="./TextArrayField.html"><code>&lt;TextArrayField&gt;</code></a></li>
<li {% if page.path == 'TextField.md' %} class="active beginner" {% else %} class="beginner" {% endif %}><a class="nav-link" href="./TextField.html"><code>&lt;TextField&gt;</code></a></li>
<li {% if page.path == 'TranslatableFields.md' %} class="active" {% endif %}><a class="nav-link" href="./TranslatableFields.html"><code>&lt;TranslatableFields&gt;</code></a></li>
<li {% if page.path == 'UrlField.md' %} class="active" {% endif %}><a class="nav-link" href="./UrlField.html"><code>&lt;UrlField&gt;</code></a></li>
Expand Down
11 changes: 11 additions & 0 deletions packages/ra-core/src/inference/inferElementFromValues.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,17 @@ const inferElementFromValues = (
)
);
}
if (
typeof values[0][0] === 'string' &&
hasType('scalar_array', types)
) {
return (
types.scalar_array &&
new InferredElement(types.scalar_array, {
source: name,
})
);
}
// FIXME introspect further
return new InferredElement(types.string, { source: name });
}
Expand Down
46 changes: 14 additions & 32 deletions packages/ra-ui-materialui/src/detail/EditGuesser.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,35 @@
import * as React from 'react';
import expect from 'expect';
import { render, screen, waitFor } from '@testing-library/react';
import { CoreAdminContext } from 'ra-core';
import { render, screen } from '@testing-library/react';

import { EditGuesser } from './EditGuesser';
import { ThemeProvider } from '../theme/ThemeProvider';
import { EditGuesser } from './EditGuesser.stories';

describe('<EditGuesser />', () => {
it('should log the guessed Edit view based on the fetched record', async () => {
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
const dataProvider = {
getOne: () =>
Promise.resolve({
data: {
id: 123,
author: 'john doe',
post_id: 6,
score: 3,
body: "Queen, tossing her head through the wood. 'If it had lost something; and she felt sure it.",
created_at: new Date('2012-08-02'),
tags_ids: [1, 2],
},
}),
getMany: () => Promise.resolve({ data: [] }),
};
render(
<ThemeProvider>
<CoreAdminContext dataProvider={dataProvider as any}>
<EditGuesser resource="comments" id={123} enableLog />
</CoreAdminContext>
</ThemeProvider>
);
await waitFor(() => {
screen.getByDisplayValue('john doe');
});
render(<EditGuesser />);
await screen.findByDisplayValue('john doe');
expect(logSpy).toHaveBeenCalledWith(`Guessed Edit:

import { DateInput, Edit, NumberInput, ReferenceArrayInput, ReferenceInput, SimpleForm, TextInput } from 'react-admin';
import { ArrayInput, BooleanInput, DateInput, Edit, NumberInput, ReferenceArrayInput, ReferenceInput, SimpleForm, SimpleFormIterator, TextArrayInput, TextInput } from 'react-admin';

export const CommentEdit = () => (
export const BookEdit = () => (
<Edit>
<SimpleForm>
<TextInput source="id" />
<TextInput source="author" />
<ArrayInput source="authors"><SimpleFormIterator><TextInput source="id" />
<TextInput source="name" />
<DateInput source="dob" /></SimpleFormIterator></ArrayInput>
<ReferenceInput source="post_id" reference="posts" />
<NumberInput source="score" />
<TextInput source="body" />
<TextInput source="description" />
<DateInput source="created_at" />
<ReferenceArrayInput source="tags_ids" reference="tags" />
<TextInput source="url" />
<TextInput source="email" />
<BooleanInput source="isAlreadyPublished" />
<TextArrayInput source="genres" />
</SimpleForm>
</Edit>
);`);
Expand Down
54 changes: 54 additions & 0 deletions packages/ra-ui-materialui/src/detail/EditGuesser.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from 'react';
import { Admin } from 'react-admin';
import { Resource, TestMemoryRouter } from 'ra-core';
import fakeRestProvider from 'ra-data-fakerest';

import { EditGuesser as RAEditGuesser } from './EditGuesser';

export default { title: 'ra-ui-materialui/detail/EditGuesser' };

const data = {
books: [
{
id: 123,
authors: [
{ id: 1, name: 'john doe', dob: '1990-01-01' },
{ id: 2, name: 'jane doe', dob: '1992-01-01' },
],
post_id: 6,
score: 3,
body: "Queen, tossing her head through the wood. 'If it had lost something; and she felt sure it.",
description: `<p><strong>War and Peace</strong> is a novel by the Russian author <a href="https://en.wikipedia.org/wiki/Leo_Tolstoy">Leo Tolstoy</a>,
published serially, then in its entirety in 1869.</p>
<p>It is regarded as one of Tolstoy's finest literary achievements and remains a classic of world literature.</p>`,
created_at: new Date('2012-08-02'),
tags_ids: [1, 2],
url: 'https://www.myshop.com/tags/top-seller',
email: '[email protected]',
isAlreadyPublished: true,
genres: [
'Fiction',
'Historical Fiction',
'Classic Literature',
'Russian Literature',
],
},
],
tags: [
{ id: 1, name: 'top seller' },
{ id: 2, name: 'new' },
],
posts: [
{ id: 6, title: 'War and Peace', body: 'A great novel by Leo Tolstoy' },
],
};

const EditGuesserWithProdLogs = () => <RAEditGuesser enableLog />;

export const EditGuesser = () => (
<TestMemoryRouter initialEntries={['/books/123']}>
<Admin dataProvider={fakeRestProvider(data)}>
<Resource name="books" edit={EditGuesserWithProdLogs} />
</Admin>
</TestMemoryRouter>
);
3 changes: 2 additions & 1 deletion packages/ra-ui-materialui/src/detail/ShowGuesser.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('<ShowGuesser />', () => {
await screen.findByText('john doe');
expect(logSpy).toHaveBeenCalledWith(`Guessed Show:

import { ArrayField, BooleanField, DataTable, DateField, EmailField, NumberField, ReferenceArrayField, ReferenceField, RichTextField, Show, SimpleShowLayout, TextField, UrlField } from 'react-admin';
import { ArrayField, BooleanField, DataTable, DateField, EmailField, NumberField, ReferenceArrayField, ReferenceField, RichTextField, Show, SimpleShowLayout, TextArrayField, TextField, UrlField } from 'react-admin';

export const BookShow = () => (
<Show>
Expand Down Expand Up @@ -39,6 +39,7 @@ export const BookShow = () => (
<UrlField source="url" />
<EmailField source="email" />
<BooleanField source="isAlreadyPublished" />
<TextArrayField source="genres" />
</SimpleShowLayout>
</Show>
);`);
Expand Down
6 changes: 6 additions & 0 deletions packages/ra-ui-materialui/src/detail/ShowGuesser.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ published serially, then in its entirety in 1869.</p>
url: 'https://www.myshop.com/tags/top-seller',
email: '[email protected]',
isAlreadyPublished: true,
genres: [
'Fiction',
'Historical Fiction',
'Classic Literature',
'Russian Literature',
],
},
],
tags: [
Expand Down
6 changes: 6 additions & 0 deletions packages/ra-ui-materialui/src/detail/editFieldTypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
SelectInput,
SimpleFormIterator,
TextInput,
TextArrayInput,
} from '../input';
import { InferredElement, InferredTypeMap, InputProps } from 'ra-core';

Expand Down Expand Up @@ -40,6 +41,11 @@ ${children.map(child => ` ${child.getRepresentation()}`).join('\n')}
.map(child => child.getRepresentation())
.join('\n')}</SimpleFormIterator></ArrayInput>`,
},
scalar_array: {
component: TextArrayInput,
representation: (props: InputProps) =>
`<TextArrayInput source="${props.source}" />`,
},
boolean: {
component: BooleanInput,
representation: (props: InputProps) =>
Expand Down
Loading
Loading