Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TextArrayInput to edit arrays of strings #10384

Merged
merged 6 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions docs/ArrayInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ To edit arrays of data embedded inside a record, `<ArrayInput>` creates a list o
}
```

**Tip**: If you need to edit an array of *strings*, like a list of email addresses or a list of tags, you should use a [`<TextArrayInput>`](./TextArrayInput.md) instead.

`<ArrayInput>` expects a single child, which must be a *form iterator* component. A form iterator is a component rendering a field array (the object returned by react-hook-form's [`useFieldArray`](https://react-hook-form.com/docs/usefieldarray)). For instance, [the `<SimpleFormIterator>` component](./SimpleFormIterator.md) displays an array of react-admin Inputs in an unordered list (`<ul>`), one sub-form by list item (`<li>`). It also provides controls for adding and removing a sub-record.

Expand Down
2 changes: 1 addition & 1 deletion docs/AutocompleteArrayInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ It renders using Material UI [Autocomplete](https://mui.com/material-ui/react-au
Your browser does not support the video tag.
</video>


This input allows editing values that are arrays of scalar values, e.g. `[123, 456]`.

**Tip**: React-admin includes other components allowing the edition of such values:

- [`<TextArrayInput>`](./TextArrayInput.md) lets you edit an array of strings
- [`<SelectArrayInput>`](./SelectArrayInput.md) renders a dropdown list of choices
- [`<CheckboxGroupInput>`](./CheckboxGroupInput.md) renders a list of checkbox options
- [`<DualListInput>`](./DualListInput.md) renders a list of choices that can be moved from one list to another
Expand Down
1 change: 0 additions & 1 deletion docs/AutocompleteInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ It renders using [Material UI's `<Autocomplete>`](https://mui.com/material-ui/re
Your browser does not support the video tag.
</video>


This input allows editing record fields that are scalar values, e.g. `123`, `'admin'`, etc.

## Usage
Expand Down
1 change: 1 addition & 0 deletions docs/CheckboxGroupInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This input allows editing values that are arrays of scalar values, e.g. `[123, 4

**Tip**: React-admin includes other components allowing the edition of such values:

- [`<TextArrayInput>`](./TextArrayInput.md) lets you edit an array of strings
- [`<SelectArrayInput>`](./SelectArrayInput.md) renders a dropdown list of choices
- [`<AutocompleteArrayInput>`](./AutocompleteArrayInput.md) renders an autocomplete input of choices
- [`<DualListInput>`](./DualListInput.md) renders a list of choices that can be moved from one list to another
Expand Down
1 change: 1 addition & 0 deletions docs/DualListInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This input allows editing values that are arrays of scalar values, e.g. `[123, 4

**Tip**: React-admin includes other components allowing the edition of such values:

- [`<TextArrayInput>`](./TextArrayInput.md) lets you edit an array of strings
- [`<AutocompleteArrayInput>`](./AutocompleteArrayInput.md) renders an Autocomplete
- [`<SelectArrayInput>`](./SelectArrayInput.md) renders a dropdown list of choices
- [`<CheckboxGroupInput>`](./CheckboxGroupInput.md) renders a list of checkbox options
Expand Down
2 changes: 1 addition & 1 deletion docs/Inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ React-admin provides a set of Input components, each one designed for a specific
| Tree node | `42` | [`<TreeInput>`](./TreeInput.md) |
| Foreign key | `42` | [`<ReferenceInput>`](./ReferenceInput.md) |
| Array of objects | `[{ item: 'jeans', qty: 3 }, { item: 'shirt', qty: 1 }]` | [`<ArrayInput>`](./ArrayInput.md) |
| Array of Enums | `['foo', 'bar']` | [`<SelectArrayInput>`](./SelectArrayInput.md), [`<AutocompleteArrayInput>`](./AutocompleteArrayInput.md), [`<CheckboxGroupInput>`](./CheckboxGroupInput.md), [`<DualListInput>`](./DualListInput.md) |
| Array of Enums | `['foo', 'bar']` | [`<TextArrayInput>`](./TextArrayinput.md), [`<SelectArrayInput>`](./SelectArrayInput.md), [`<AutocompleteArrayInput>`](./AutocompleteArrayInput.md), [`<CheckboxGroupInput>`](./CheckboxGroupInput.md), [`<DualListInput>`](./DualListInput.md) |
| Array of foreign keys | `[42, 43]` | [`<ReferenceArrayInput>`](./ReferenceArrayInput.md) |
| Translations | `{ en: 'Hello', fr: 'Bonjour' }` | [`<TranslatableInputs>`](./TranslatableInputs.md) |
| Related records | `[{ id: 42, title: 'Hello' }, { id: 43, title: 'World' }]` | [`<ReferenceManyInput>`](./ReferenceManyInput.md), [`<ReferenceManyToManyInput>`](./ReferenceManyToManyInput.md), [`<ReferenceNodeInput>`](./ReferenceNodeInput.md), [`<ReferenceOneInput>`](./ReferenceOneInput.md) |
Expand Down
1 change: 1 addition & 0 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ title: "Index"
* [`<TabbedForm>`](./TabbedForm.md)
* [`<TabbedFormWithRevision>`](./TabbedForm.md#versioning)<img class="icon" src="./img/premium.svg" />
* [`<TabbedShowLayout>`](./TabbedShowLayout.md)
* [`<TextArrayInput>`](./TextArrayInput.md)
* [`<TextField>`](./TextField.md)
* [`<TextInput>`](./TextInput.md)
* [`<TimeInput>`](./TimeInput.md)
Expand Down
1 change: 1 addition & 0 deletions docs/SelectArrayInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This input allows editing values that are arrays of scalar values, e.g. `[123, 4

**Tip**: React-admin includes other components allowing the edition of such values:

- [`<TextArrayInput>`](./TextArrayInput.md) lets you edit an array of strings
- [`<AutocompleteArrayInput>`](./AutocompleteArrayInput.md) renders an Autocomplete
- [`<CheckboxGroupInput>`](./CheckboxGroupInput.md) renders a list of checkbox options
- [`<DualListInput>`](./DualListInput.md) renders a list of choices that can be moved from one list to another
Expand Down
110 changes: 110 additions & 0 deletions docs/TextArrayInput.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
layout: default
title: "The TextArrayInput Component"
---

# `<TextArrayInput>`

`<TextArrayInput>` lets you edit an array of strings, like a list of email addresses or a list of tags. It renders as an input where the current values are represented as chips. Users can add or delete new values.

<video controls autoplay playsinline muted loop>
<source src="./img/TextArrayInput.mp4" type="video/mp4"/>
Your browser does not support the video tag.
</video>


## Usage

Use `<TextArrayInput>` to edit an array of strings:

```jsx
import { Create, SimpleForm, TextArrayInput, TextInput } from 'react-admin';

export const EmailCreate = () => (
<Create>
<SimpleForm>
<TextArrayInput source="to" />
<TextInput source="subject" />
<TextInput source="body" multiline minRows={5} />
</SimpleForm>
</Create>
);
```

This form will allow users to input multiple email addresses in the `to` field. The resulting email will look like this:

```jsx
{
"to": ["[email protected]", "[email protected]"],
"subject": "Request for a quote",
"body": "Hi,\n\nI would like to know if you can provide a quote for the following items:\n\n- 100 units of product A\n- 50 units of product B\n- 25 units of product C\n\nBest regards,\n\nJulie\n",
"id": 123,
"date": "2024-11-26T11:37:22.564Z",
"from": "[email protected]",
}
```

`<TextArrayInput>` is designed for simple string arrays. For more complex use cases, consider the following alternatives:

- [`<SelectArrayInput>`](./SelectArrayInput.md) or [`<AutocompleteArrayInput>`](./AutocompleteArrayInput.md) if the possible values are limited to a predefined list.
- [`<ReferenceArrayInput>`](./ReferenceArrayInput.md) if the possible values are stored in another resource.
- [`<ArrayInput>`](./ArrayInput.md) if the stored value is an array of *objects* instead of an array of strings.

## Props

| Prop | Required | Type | Default | Description |
| ------------ | -------- | --------- | ------- | -------------------------------------------------------------------- |
| `options` | Optional | `string[]` | | Optional list of possible values for the input. If provided, the input will suggest these values as the user types. |
| `renderTags` | Optional | `(value, getTagProps) => ReactNode` | | A function to render selected value. |

`<TextArrayInput>` also accepts the [common input props](./Inputs.md#common-input-props).

Additional props are passed down to the underlying Material UI [`<Autocomplete>`](https://mui.com/material-ui/react-autocomplete/) component.

## `options`

You can make show a list of suggestions to the user by setting the `options` prop:

```jsx
<TextArrayInput
source="to"
options={[
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
]}
/>
```

## `renderTags`

To customize the rendering of the chips, use the `renderTags` prop. This prop is a function that takes two arguments:

- `value`: The input value (an array of strings)
- `getTagProps`: A props getter for an individual tag.

```tsx
<TextArrayInput
source="to"
renderTags={(value: readonly string[], getTagProps) =>
value.map((option: string, index: number) => {
const { key, ...tagProps } = getTagProps({ index });
return (
<Chip
variant="outlined"
label={option}
key={key}
{...tagProps}
/>
);
})
}
/>
```
Binary file added docs/img/TextArrayInput.mp4
Binary file not shown.
1 change: 1 addition & 0 deletions docs/navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@
<li {% if page.path == 'SelectArrayInput.md' %} class="active beginner" {% else %} class="beginner" {% endif %}><a class="nav-link" href="./SelectArrayInput.html"><code>&lt;SelectArrayInput&gt;</code></a></li>
<li {% if page.path == 'SimpleFormIterator.md' %} class="active beginner" {% else %} class="beginner" {% endif %}><a class="nav-link" href="./SimpleFormIterator.html"><code>&lt;SimpleFormIterator&gt;</code></a></li>
<li {% if page.path == 'SmartRichTextInput.md' %} class="active" {% endif %}><a class="nav-link" href="./SmartRichTextInput.html"><code>&lt;SmartRichTextInput&gt;</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'TextArrayInput.md' %} class="active beginner" {% else %} class="beginner" {% endif %}><a class="nav-link" href="./TextArrayInput.html"><code>&lt;TextArrayInput&gt;</code></a></li>
<li {% if page.path == 'TextInput.md' %} class="active beginner" {% else %} class="beginner" {% endif %}><a class="nav-link" href="./TextInput.html"><code>&lt;TextInput&gt;</code></a></li>
<li {% if page.path == 'TimeInput.md' %} class="active" {% endif %}><a class="nav-link" href="./TimeInput.html"><code>&lt;TimeInput&gt;</code></a></li>
<li {% if page.path == 'TranslatableInputs.md' %} class="active" {% endif %}><a class="nav-link" href="./TranslatableInputs.html"><code>&lt;TranslatableInputs&gt;</code></a></li>
Expand Down
2 changes: 1 addition & 1 deletion examples/simple/src/data.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export default {
posts: [
{
id: 1,
id: '1 1',
slax57 marked this conversation as resolved.
Show resolved Hide resolved
title: 'Accusantium qui nihil voluptatum quia voluptas maxime ab similique',
teaser: 'In facilis aut aut odit hic doloribus. Fugit possimus perspiciatis sit molestias in. Sunt dignissimos sed quis at vitae veniam amet. Sint sunt perspiciatis quis doloribus aperiam numquam consequatur et. Blanditiis aut earum incidunt eos magnam et voluptatem. Minima iure voluptatum autem. At eaque sit aperiam minima aut in illum.',
body: '<p>Rerum velit quos est <strong>similique</strong>. Consectetur tempora eos ullam velit nobis sit debitis. Magni explicabo omnis delectus labore vel recusandae.</p><p>Aut a minus laboriosam harum placeat quas minima fuga. Quos nulla fuga quam officia tempore. Rerum occaecati ut eum et tempore. Nam ab repudiandae et nemo praesentium.</p><p>Cumque corporis officia occaecati ducimus sequi laborum omnis ut. Nam aspernatur veniam fugit. Nihil eum libero ea dolorum ducimus impedit sed. Quidem inventore porro corporis debitis eum in. Nesciunt unde est est qui nulla. Esse sunt placeat molestiae molestiae sed quia. Sunt qui quidem quos velit reprehenderit quos blanditiis ducimus. Sint et molestiae maxime ut consequatur minima. Quaerat rem voluptates voluptatem quos. Corporis perferendis in provident iure. Commodi odit exercitationem excepturi et deserunt qui.</p><p>Optio iste necessitatibus velit non. Neque sed occaecati culpa porro culpa. Quia quam in molestias ratione et necessitatibus consequatur. Est est tempora consequatur voluptatem vel. Mollitia tenetur non quis omnis perspiciatis deserunt sed necessitatibus. Ad rerum reiciendis sunt aspernatur.</p><p>Est ullam ut magni aspernatur. Eum et sed tempore modi.</p><p>Earum aperiam sit neque quo laborum suscipit unde. Expedita nostrum itaque non non adipisci. Ut delectus quis delectus est at sint. Iste hic qui ea eaque eaque sed id. Hic placeat rerum numquam id velit deleniti voluptatem. Illum adipisci voluptas adipisci ut alias. Earum exercitationem iste quidem eveniet aliquid hic reiciendis. Exercitationem est sunt in minima consequuntur. Aut quaerat libero dolorem.</p>',
Expand Down
62 changes: 62 additions & 0 deletions packages/ra-ui-materialui/src/input/TextArrayInput.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';

import { Basic, HelperText, Label, Required } from './TextArrayInput.stories';

describe('<TextArrayInput />', () => {
it('should render the values as chips', () => {
render(<Basic />);
const chip1 = screen.getByText('[email protected]');
expect(chip1.classList.contains('MuiChip-label')).toBe(true);
const chip2 = screen.getByText('[email protected]');
expect(chip2.classList.contains('MuiChip-label')).toBe(true);
});
it('should allow to remove a value', async () => {
render(<Basic />);
await screen.findByText(
'["[email protected]","[email protected]"] (object)'
);
const deleteButtons = screen.getAllByTestId('CancelIcon');
fireEvent.click(deleteButtons[0]);
await screen.findByText('["[email protected]"] (object)');
});
it('should allow to remove all values one by one', async () => {
render(<Basic />);
await screen.findByText(
'["[email protected]","[email protected]"] (object)'
);
const deleteButtons = screen.getAllByTestId('CancelIcon');
fireEvent.click(deleteButtons[1]);
fireEvent.click(deleteButtons[0]);
await screen.findByText('[] (object)');
});
it('should allow to remove all values using the reset button', async () => {
render(<Basic />);
const input = screen.getByLabelText('resources.emails.fields.to');
fireEvent.click(input);
const clearButton = screen.getByLabelText('Clear');
fireEvent.click(clearButton);
await screen.findByText('[] (object)');
});
it('should allow to add a value', async () => {
render(<Basic />);
const input = screen.getByLabelText('resources.emails.fields.to');
fireEvent.change(input, { target: { value: '[email protected]' } });
fireEvent.keyDown(input, { key: 'Enter' });
await screen.findByText(
'["[email protected]","[email protected]","[email protected]"] (object)'
);
});
it('should render the helper text', () => {
render(<HelperText />);
screen.getByText('Email addresses of the recipients');
});
it('should render the custom label', () => {
render(<Label />);
screen.getByText('To');
});
it('should show required fields as required', () => {
render(<Required />);
expect(screen.getAllByText('*').length).toBe(2);
});
});
Loading
Loading