Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Copy Markdown
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.

@slax57 slax57 Sep 17, 2025 •

Copy link
Copy Markdown
Contributor Author

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
Copy Markdown
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](https://mui.com/material-ui/react-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](https://mui.com/material-ui/react-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](https://mui.com/material-ui/react-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: 'doe@production.com',
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: 'doe@production.com',
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