diff --git a/docs/ArrayInput.md b/docs/ArrayInput.md index 8d048781fb4..1dc23c2596a 100644 --- a/docs/ArrayInput.md +++ b/docs/ArrayInput.md @@ -38,6 +38,7 @@ To edit arrays of data embedded inside a record, `` 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.md) instead. `` 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 `` component](./SimpleFormIterator.md) displays an array of react-admin Inputs in an unordered list (`
    `), one sub-form by list item (`
  • `). It also provides controls for adding and removing a sub-record. diff --git a/docs/AutocompleteArrayInput.md b/docs/AutocompleteArrayInput.md index 005b0d464a1..2ae96fc221b 100644 --- a/docs/AutocompleteArrayInput.md +++ b/docs/AutocompleteArrayInput.md @@ -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. - 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.md) lets you edit an array of strings - [``](./SelectArrayInput.md) renders a dropdown list of choices - [``](./CheckboxGroupInput.md) renders a list of checkbox options - [``](./DualListInput.md) renders a list of choices that can be moved from one list to another diff --git a/docs/AutocompleteInput.md b/docs/AutocompleteInput.md index 02c5303aaa7..387c7fa8fe9 100644 --- a/docs/AutocompleteInput.md +++ b/docs/AutocompleteInput.md @@ -14,7 +14,6 @@ It renders using [Material UI's ``](https://mui.com/material-ui/re Your browser does not support the video tag. - This input allows editing record fields that are scalar values, e.g. `123`, `'admin'`, etc. ## Usage diff --git a/docs/CheckboxGroupInput.md b/docs/CheckboxGroupInput.md index 9b6d99d0732..4a645b83080 100644 --- a/docs/CheckboxGroupInput.md +++ b/docs/CheckboxGroupInput.md @@ -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.md) lets you edit an array of strings - [``](./SelectArrayInput.md) renders a dropdown list of choices - [``](./AutocompleteArrayInput.md) renders an autocomplete input of choices - [``](./DualListInput.md) renders a list of choices that can be moved from one list to another diff --git a/docs/DualListInput.md b/docs/DualListInput.md index d19c6bcecfa..e4a7ea4f71b 100644 --- a/docs/DualListInput.md +++ b/docs/DualListInput.md @@ -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.md) lets you edit an array of strings - [``](./AutocompleteArrayInput.md) renders an Autocomplete - [``](./SelectArrayInput.md) renders a dropdown list of choices - [``](./CheckboxGroupInput.md) renders a list of checkbox options diff --git a/docs/Inputs.md b/docs/Inputs.md index 41e87eb5437..30186752cd0 100644 --- a/docs/Inputs.md +++ b/docs/Inputs.md @@ -79,7 +79,7 @@ React-admin provides a set of Input components, each one designed for a specific | Tree node | `42` | [``](./TreeInput.md) | | Foreign key | `42` | [``](./ReferenceInput.md) | | Array of objects | `[{ item: 'jeans', qty: 3 }, { item: 'shirt', qty: 1 }]` | [``](./ArrayInput.md) | -| Array of Enums | `['foo', 'bar']` | [``](./SelectArrayInput.md), [``](./AutocompleteArrayInput.md), [``](./CheckboxGroupInput.md), [``](./DualListInput.md) | +| Array of Enums | `['foo', 'bar']` | [``](./TextArrayinput.md), [``](./SelectArrayInput.md), [``](./AutocompleteArrayInput.md), [``](./CheckboxGroupInput.md), [``](./DualListInput.md) | | Array of foreign keys | `[42, 43]` | [``](./ReferenceArrayInput.md) | | Translations | `{ en: 'Hello', fr: 'Bonjour' }` | [``](./TranslatableInputs.md) | | Related records | `[{ id: 42, title: 'Hello' }, { id: 43, title: 'World' }]` | [``](./ReferenceManyInput.md), [``](./ReferenceManyToManyInput.md), [``](./ReferenceNodeInput.md), [``](./ReferenceOneInput.md) | diff --git a/docs/Reference.md b/docs/Reference.md index 3983cda09a1..0759ec1b43e 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -188,6 +188,7 @@ title: "Index" * [``](./TabbedForm.md) * [``](./TabbedForm.md#versioning) * [``](./TabbedShowLayout.md) +* [``](./TextArrayInput.md) * [``](./TextField.md) * [``](./TextInput.md) * [``](./TimeInput.md) diff --git a/docs/SelectArrayInput.md b/docs/SelectArrayInput.md index a3f0ad11bde..e8451d116bf 100644 --- a/docs/SelectArrayInput.md +++ b/docs/SelectArrayInput.md @@ -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.md) lets you edit an array of strings - [``](./AutocompleteArrayInput.md) renders an Autocomplete - [``](./CheckboxGroupInput.md) renders a list of checkbox options - [``](./DualListInput.md) renders a list of choices that can be moved from one list to another diff --git a/docs/TextArrayInput.md b/docs/TextArrayInput.md new file mode 100644 index 00000000000..5936ed949f2 --- /dev/null +++ b/docs/TextArrayInput.md @@ -0,0 +1,110 @@ +--- +layout: default +title: "The TextArrayInput Component" +--- + +# `` + +`` 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. + + + + +## Usage + +Use `` to edit an array of strings: + +```jsx +import { Create, SimpleForm, TextArrayInput, TextInput } from 'react-admin'; + +export const EmailCreate = () => ( + + + + + + + +); +``` + +This form will allow users to input multiple email addresses in the `to` field. The resulting email will look like this: + +```jsx +{ + "to": ["jane.smith@example.com", "john.doe@acme.com"], + "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": "julie.green@example.com", +} +``` + +`` is designed for simple string arrays. For more complex use cases, consider the following alternatives: + +- [``](./SelectArrayInput.md) or [``](./AutocompleteArrayInput.md) if the possible values are limited to a predefined list. +- [``](./ReferenceArrayInput.md) if the possible values are stored in another resource. +- [``](./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. | + +`` also accepts the [common input props](./Inputs.md#common-input-props). + +Additional props are passed down to the underlying Material UI [``](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 + +``` + +## `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 + + value.map((option: string, index: number) => { + const { key, ...tagProps } = getTagProps({ index }); + return ( + + ); + }) + } +/> +``` \ No newline at end of file diff --git a/docs/img/TextArrayInput.mp4 b/docs/img/TextArrayInput.mp4 new file mode 100644 index 00000000000..06fc4ed5e2a Binary files /dev/null and b/docs/img/TextArrayInput.mp4 differ diff --git a/docs/navigation.html b/docs/navigation.html index 3f0bda74884..9d31aacd1ae 100644 --- a/docs/navigation.html +++ b/docs/navigation.html @@ -212,6 +212,7 @@
  • <SelectArrayInput>
  • <SimpleFormIterator>
  • <SmartRichTextInput>
  • +
  • <TextArrayInput>
  • <TextInput>
  • <TimeInput>
  • <TranslatableInputs>
  • diff --git a/packages/ra-ui-materialui/src/input/TextArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/TextArrayInput.spec.tsx new file mode 100644 index 00000000000..91942edc86a --- /dev/null +++ b/packages/ra-ui-materialui/src/input/TextArrayInput.spec.tsx @@ -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('', () => { + it('should render the values as chips', () => { + render(); + const chip1 = screen.getByText('john@example.com'); + expect(chip1.classList.contains('MuiChip-label')).toBe(true); + const chip2 = screen.getByText('albert@target.dev'); + expect(chip2.classList.contains('MuiChip-label')).toBe(true); + }); + it('should allow to remove a value', async () => { + render(); + await screen.findByText( + '["john@example.com","albert@target.dev"] (object)' + ); + const deleteButtons = screen.getAllByTestId('CancelIcon'); + fireEvent.click(deleteButtons[0]); + await screen.findByText('["albert@target.dev"] (object)'); + }); + it('should allow to remove all values one by one', async () => { + render(); + await screen.findByText( + '["john@example.com","albert@target.dev"] (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(); + 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(); + const input = screen.getByLabelText('resources.emails.fields.to'); + fireEvent.change(input, { target: { value: 'bob.brown@example.com' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + await screen.findByText( + '["john@example.com","albert@target.dev","bob.brown@example.com"] (object)' + ); + }); + it('should render the helper text', () => { + render(); + screen.getByText('Email addresses of the recipients'); + }); + it('should render the custom label', () => { + render(