diff --git a/docs/AutocompleteArrayInput.md b/docs/AutocompleteArrayInput.md index 04881167106..93bf823cc33 100644 --- a/docs/AutocompleteArrayInput.md +++ b/docs/AutocompleteArrayInput.md @@ -10,96 +10,312 @@ It renders using MUI [Autocomplete](https://mui.com/components/autocomplete/). ![AutocompleteArrayInput](./img/autocomplete-array-input.gif) -Set the `choices` attribute to determine the options list (with `id`, `name` tuples). +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: + +- [``](./SelectArrayInput.md) renders a dropdown list of choices +- [``](./CheckboxGroupInput.md) renders a list of checkbox options + +**Tip**: `` is a stateless component, so it only allows to *filter* the list of choices, not to *extend* it. If you need to populate the list of choices based on the result from a `fetch` call (and if [``](./ReferenceArrayInput.md) doesn't cover your need), you'll have to [write your own Input component](./Inputs.md#writing-your-own-input-component) based on MUI `` component. +## Usage + +In addition to the `source`, `` requires one prop: the `choices` listing the possible values. ```jsx import { AutocompleteArrayInput } from 'react-admin'; - ``` -## Properties - -| Prop | Required | Type | Default | Description | -|---------------------------|----------|----------------------------|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `choices` | Required | `Object[]` | - | List of items to auto-suggest | -| `create` | Optional | `Element` | `-` | A React Element to render when users want to create a new choice | -| `createLabel` | Optional | `string` | `ra.action.create` | The label for the menu item allowing users to create a new choice. Used when the filter is empty | -| `createItemLabel` | Optional | `string` | `ra.action.create_item` | The label for the menu item allowing users to create a new choice. Used when the filter is not empty | -| `debounce` | Optional | `number` | `250` | The delay to wait before calling the setFilter function injected when used in a ReferenceInput. | -| `emptyText` | Optional | `string` | `''` | The text to use for the empty element | -| `emptyValue` | Optional | `any` | `''` | The value to use for the empty element | -| `inputText` | Optional | `Function` | `-` | Required if `optionText` is a custom Component, this function must return the text displayed for the current selection. | -| `matchSuggestion` | Optional | `Function` | - | Required if `optionText` is a React element. Function returning a boolean indicating whether a choice matches the filter. `(filter, choice) => boolean` | -| `onCreate` | Optional | `Function` | `-` | A function called with the current filter value when users choose to create a new choice. | -| `optionText` | Optional | `string` | `Function` | `Component` | `name` | Field name of record to display in the suggestion item or function which accepts the correct record as argument (`(record)=> {string}`) | -| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | -| `filterToQuery` | Optional | `string` => `Object` | `searchText => ({ q: [searchText] })` | How to transform the searchText into a parameter for the data provider | -| `setFilter` | Optional | `Function` | `null` | A callback to inform the `searchText` has changed and new `choices` can be retrieved based on this `searchText`. Signature `searchText => void`. This function is automatically setup when using `ReferenceInput`. | -| `shouldRenderSuggestions` | Optional | `Function` | `() => true` | A function that returns a `boolean` to determine whether or not suggestions are rendered. Use this when working with large collections of data to improve performance and user experience. This function is passed into the underlying `MUI Autocomplete` component. Ex.`(value) => value.trim().length > 2` | -| `suggestionLimit` | Optional | `number` | `null` | Limits the numbers of suggestions that are shown in the dropdown list | +By default, the possible choices are built from the `choices` prop, using: + - the `id` field as the option value, + - the `name` field as the option text + +The form value for the source must be an array of the selected values, e.g. + +```js +{ + id: 123, + name: 'John Doe', + roles: ['u001', 'u003'], +} +``` + +## Props + +| Prop | Required | Type | Default | Description | +|----------------------------|----------|-----------------------|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `choices` | Required | `Object[]` | - | List of choices | +| `create` | Optional | `Element` | `-` | A React Element to render when users want to create a new choice | +| `createLabel` | Optional | `string` | `ra.action. create` | The label for the menu item allowing users to create a new choice. Used when the filter is empty | +| `createItemLabel` | Optional | `string` | `ra.action .create_item` | The label for the menu item allowing users to create a new choice. Used when the filter is not empty | +| `debounce` | Optional | `number` | `250` | The delay to wait before calling the setFilter function injected when used in a ReferenceArray Input. | +| `emptyText` | Optional | `string` | `''` | The text to use for the empty element | +| `emptyValue` | Optional | `any` | `''` | The value to use for the empty element | +| `filterToQuery` | Optional | `string` => `Object` | `q => ({ q })` | How to transform the searchText into a parameter for the data provider | +| `inputText` | Optional | `Function` | `-` | Required if `optionText` is a custom Component, this function must return the text displayed for the current selection. | +| `matchSuggestion` | Optional | `Function` | `-` | Required if `optionText` is a React element. Function returning a boolean indicating whether a choice matches the filter. `(filter, choice) => boolean` | +| `onCreate` | Optional | `Function` | `-` | A function called with the current filter value when users choose to create a new choice. | +| `optionText` | Optional | `string` | `Function` | `Component` | `name` | Field name of record to display in the suggestion item or function which accepts the correct record as argument (`(record)=> {string}`) | +| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | +| `setFilter` | Optional | `Function` | `null` | A callback to inform the `searchText` has changed and new `choices` can be retrieved based on this `searchText`. Signature `searchText => void`. This function is automatically set up when using `ReferenceArray Input`. | +| `shouldRender Suggestions` | Optional | `Function` | `() => true` | A function that returns a `boolean` to determine whether or not suggestions are rendered. | +| `suggestionLimit` | Optional | `number` | `null` | Limits the numbers of suggestions that are shown in the dropdown list | +| `translateChoice` | Optional | `boolean` | `true` | Whether the choices should be translated | + `` also accepts the [common input props](./Inputs.md#common-input-props). -## Usage +## `choices` -`` is designed to for fields containing an array of scalar values, e.g.: +The list of choices must be an array of objects - one object for each possible choice. In each object, `id` is the value, and the `name` is the label displayed to the user. -```json -{ - "id": 123, - "tags": ["lifestyle", "photography"] -} +```jsx + ``` -When working with a field that contains an array of *objects*, use `parse` and `format` to turn the value into an array of scalar values. +You can also use an array of objects with different properties for the label and value, given you specify the `optionText` and `optionValue` props: -So for instance, for editing the `tags` field of records looking like the following: +```jsx + +``` -```json -{ - "id": 123, - "tags": [ - { "id": "lifestyle" }, - { "id": "photography" } - ] -} +The choices are translated by default, so you can use translation identifiers as choices: + +```jsx +const choices = [ + { id: 'admin', name: 'myroot.roles.admin' }, + { id: 'u001', name: 'myroot.roles.u001' }, + { id: 'u002', name: 'myroot.roles.u002' }, + { id: 'u003', name: 'myroot.roles.u003' }, +]; ``` -You should use the following syntax: +You can opt-out of this translation by setting [the `translateChoice` prop](#translatechoice) to `false`. + +If you need to *fetch* the options from another resource, you're actually editing a one-to-many or a many-to-many relationship. In this case, wrap the `` in a [``](./ReferenceArrayInput.md) or a [``](./ReferenceManyToManyInput.md) component. You don't need to specify the `choices` prop - the parent component injects it based on the possible values of the related resource. ```jsx -import { AutocompleteArrayInput } from 'react-admin'; + + + +``` - - value && value.map(v => ({ id: v })) - } - format={value => value && value.map(v => v.id)} - choices={[ - { id: 'programming', name: 'Programming' }, - { id: 'lifestyle', name: 'Lifestyle' }, - { id: 'photography', name: 'Photography' }, - ]} -/> +If you have an *array of values* for the options, turn it into an array of objects with the `id` and `name` properties: + +```jsx +const possibleValues = ['programming', 'lifestyle', 'photography']; +const ucfirst = name => name.charAt(0).toUpperCase() + name.slice(1); +const choices = possibleValues.map(value => ({ id: value, name: ucfirst(value) })); + + +``` + +## `create` + +To allow users to add new options, pass a React element as the `create` prop. `` will then render a menu item at the bottom of the list, which will render the passed element when clicked. + +{% raw %} +```jsx +import { CreateCategory } from './CreateCategory'; + +const PostCreate = () => ( + + + + + } /> + + + +); + +// in ./CreateCategory.js +import { useCreate, useCreateSuggestionContext } from 'react-admin'; +import { + Box, + BoxProps, + Button, + Dialog, + DialogActions, + DialogContent, + TextField, +} from '@mui/material'; + +const CreateCategory = () => { + const { filter, onCancel, onCreate } = useCreateSuggestionContext(); + const [create] = useCreate(); + const [value, setValue] = React.useState(filter || ''); + + const handleSubmit = event => { + event.preventDefault(); + create( + 'categories', + { data: { title: value } }, + { + onSuccess: (data) => { + setValue(''); + onCreate(data); + }, + } + ); + }; + + return ( + +
+ + setValue(event.target.value)} + autoFocus + /> + + + + + +
+
+ ); +}; +``` +{% endraw %} + +If you just need to ask users for a single string to create the new option, you can use [the `onCreate` prop](#oncreate) instead. + +## `debounce` + +When used inside a [``](./ReferenceArrayInput.md), `` will call `dataProvider.getList()` with the current input value as filter after a delay of 250ms. This is to avoid calling the API too often while users are typing their query. + +This delay can be customized by setting the `debounce` prop. + +```jsx + + + +``` + +## `emptyText` + +If the input isn't required (using `validate={required()}`), and you need a choice to represent the empty value, set `emptyText` prop and a choice will be added at the top, with its value as label. + +```jsx + +``` + +The `emptyText` prop accepts either a string or a React Element. + +And if you want to hide that empty choice, make the input required. + +```jsx + +``` + +## `emptyValue` + +If the input isn't required (using `validate={required()}`), users can select an empty choice. The default value for that empty choice is the empty string (`''`), or `null` if the input is inside a [``](./ReferenceArrayInput.md). + +You can override this value with the `emptyValue` prop. + +```jsx + +``` + +**Tip**: While you can set `emptyValue` to a non-string value (e.g. `0`), you cannot use `null` or `undefined`, as it would turn the `` into an [uncontrolled component](https://reactjs.org/docs/uncontrolled-components.html). If you need the empty choice to be stored as `null` or `undefined`, use [the `parse` prop](./Inputs.md#parse) to convert the default empty value ('') to `null` or `undefined`, or use [the `sanitizeEmptyValues` prop](./SimpleForm.md#sanitizeemptyvalues) on the Form component. + +## `filterToQuery` + +When used inside a [``](./ReferenceArrayInput.md), whenever users type a string in the autocomplete input, `` calls `dataProvider.getList()` using the string as filter, to return a filtered list of possible options from the reference resource. This filter is built using the `filterToQuery` prop. + +By default, the filter is built using the `q` parameter. This means that if the user types the string 'lorem', the filter will be `{ q: 'lorem' }`. + +You can customize the filter by setting the `filterToQuery` prop. It should be a function that returns a filter object. + +```jsx +const filterToQuery = searchText => ({ name_ilike: `%${searchText}%` }); + + + + +``` + +## `onCreate` + +Use the `onCreate` prop to allow users to create new options on-the-fly. Its value must be a function. This lets you render a `prompt` to ask users about the new value. You can return either the new choice directly or a Promise resolving to the new choice. + +{% raw %} +```js +import { AutocompleteArrayInput, Create, SimpleForm, TextInput } from 'react-admin'; + +const PostCreate = () => { + const categories = [ + { name: 'Tech', id: 'tech' }, + { name: 'Lifestyle', id: 'lifestyle' }, + ]; + return ( + + + + { + const newCategoryName = prompt('Enter a new category'); + const newCategory = { id: newCategoryName.toLowerCase(), name: newCategoryName }; + categories.push(newCategory); + return newCategory; + }} + source="category_ids" + choices={categories} + /> + + + ); +} ``` +{% endraw %} + +If a prompt is not enough, you can use [the `create` prop](#create) to render a custom component instead. -You can customize the properties to use for the option name and value, thanks to the `optionText` and `optionValue` attributes: +## `optionText` + +You can customize the properties to use for the option name (instead of the default `name`) thanks to the `optionText` prop: ```jsx const choices = [ - { _id: 123, full_name: 'Leo Tolstoi', sex: 'M' }, - { _id: 456, full_name: 'Jane Austen', sex: 'F' }, + { id: 'admin', label: 'Admin' }, + { id: 'u001', label: 'Editor' }, + { id: 'u002', label: 'Moderator' }, + { id: 'u003', label: 'Reviewer' }, ]; - + ``` -`optionText` also accepts a function, so you can shape the option text at will: +`optionText` is especially useful when the choices are records coming from a `` or a ``. By default, react-admin uses the [`recordRepresentation`](./Resource.md#recordrepresentation) function to display the record label. But if you set the `optionText` prop, react-admin will use it instead. + +```jsx + + + +``` + +`optionText` also accepts a function, so you can shape the option text based on the entire choice object: ```jsx const choices = [ @@ -107,28 +323,105 @@ const choices = [ { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`; - + + ``` +`optionText` also accepts a React Element, that will be rendered inside a [``](./useRecordContext.md) using the related choice as the `record` prop. You can use Field components there. + +```jsx +const choices = [ + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, +]; + +const FullNameField = () => { + const record = useRecordContext(); + return {record.first_name} {record.last_name}; +} + +}/> +``` + +## `optionValue` + +You can customize the properties to use for the option value (instead of the default `id`) thanks to the `optionValue` prop: + +```jsx +const choices = [ + { _id: 'admin', name: 'Admin' }, + { _id: 'u001', name: 'Editor' }, + { _id: 'u002', name: 'Moderator' }, + { _id: 'u003', name: 'Reviewer' }, +]; + +``` + +## `shouldRenderSuggestions` + +When dealing with a large amount of `choices` you may need to limit the number of suggestions that are rendered in order to maintain acceptable performance. `shouldRenderSuggestions` is an optional prop that allows you to set conditions on when to render suggestions. An easy way to improve performance would be to skip rendering until the user has entered 2 or 3 characters in the search box. This lowers the result set significantly and might be all you need (depending on your data set). + +```jsx + { return val.trim().length > 2 }} +/> +``` + +This prop is passed to the underlying `react-autosuggest` component and is documented [in their respository](https://github.com/moroshko/react-autosuggest#should-render-suggestions-prop). + +## `suggestionLimit` + +The `choices` prop can be very large, and rendering all of them would be very slow. To limit the number of suggestions displayed at any time, set the `suggestionLimit` prop: + +```jsx + +``` + +If you're using `` inside a [``](./ReferenceArrayInput.md), limit the number of choices returned by the API instead, using the `perPage` prop of the ``. + +```jsx + + + +``` + +## `sx`: CSS API + +The `` component accepts the usual `className` prop. You can also override many styles of the inner components thanks to the `sx` property (as most MUI components, see their [documentation about it](https://mui.com/customization/how-to-customize/#overriding-nested-component-styles)). + +`` renders an [``](./AutocompleteInput.md) and reuses its styles. To override the style of all instances of `` using the [MUI style overrides](https://mui.com/customization/globals/#css), use the `RaAutocompleteInput` key. + +Refer to the [MUI `` component](https://mui.com/components/autocomplete/) to know its CSS API. + +## `translateChoice` + The choices are translated by default, so you can use translation identifiers as choices: ```jsx const choices = [ - { id: 'M', name: 'myroot.gender.male' }, - { id: 'F', name: 'myroot.gender.female' }, + { id: 'admin', name: 'myroot.roles.admin' }, + { id: 'u001', name: 'myroot.roles.u001' }, + { id: 'u002', name: 'myroot.roles.u002' }, + { id: 'u003', name: 'myroot.roles.u003' }, ]; ``` -However, in some cases (e.g. inside a ``), you may not want the choice to be translated. In that case, set the `translateChoice` prop to `false`. +However, in some cases (e.g. inside a ``), you may not want the choice to be translated. +In that case, set the `translateChoice` prop to `false`. ```jsx - + ``` -When dealing with a large amount of `choices` you may need to limit the number of suggestions that are rendered in order to maintain usable performance. The `shouldRenderSuggestions` is an optional prop that allows you to set conditions on when to render suggestions. An easy way to improve performance would be to skip rendering until the user has entered 2 or 3 characters in the search box. This lowers the result set significantly, and might be all you need (depending on your data set). -Ex. ` { return val.trim().length > 2 }} />` would not render any suggestions until the 3rd character has been entered. This prop is passed to the underlying `react-autosuggest` component and is documented [here](https://github.com/moroshko/react-autosuggest#should-render-suggestions-prop). +## Additional Props -Lastly, `` renders a [MUI `` component](https://mui.com/components/autocomplete/) and accepts the `` props: +`` renders a [MUI `` component](https://mui.com/components/autocomplete/) and accepts the `` props: {% raw %} ```jsx @@ -138,7 +431,9 @@ Lastly, `` renders a [MUI `` component](ht **Tip**: Like many other inputs, `` accept a `fullWidth` prop. -**Tip**: If you want to populate the `choices` attribute with a list of related records, you should decorate `` with [``](./ReferenceArrayInput.md), and leave the `choices` empty: +## Using In A ReferenceArrayInput + +If you want to populate the `choices` attribute with a list of related records, you should decorate `` with [``](./ReferenceArrayInput.md), and leave the `choices` empty: ```jsx import { AutocompleteArrayInput, ReferenceArrayInput } from 'react-admin'; @@ -148,9 +443,79 @@ import { AutocompleteArrayInput, ReferenceArrayInput } from 'react-admin'; ``` -**Tip**: `` is a stateless component, so it only allows to *filter* the list of choices, not to *extend* it. If you need to populate the list of choices based on the result from a `fetch` call (and if [``](./ReferenceArrayInput.md) doesn't cover your need), you'll have to [write your own Input component](./Inputs.md#writing-your-own-input-component) based on [material-ui-chip-input](https://github.com/TeamWertarbyte/material-ui-chip-input). +## Working With Object Values + +When working with a field that contains an array of *objects*, use `parse` and `format` to turn the value into an array of scalar values. + +So for instance, for editing the `tags` field of records looking like the following: + +```json +{ + "id": 123, + "tags": [ + { "id": "lifestyle" }, + { "id": "photography" } + ] +} +``` + +You should use the following syntax: + +```jsx +import { AutocompleteArrayInput } from 'react-admin'; + + + value && value.map(v => ({ id: v })) + } + format={value => value && value.map(v => v.id)} + choices={[ + { id: 'programming', name: 'Programming' }, + { id: 'lifestyle', name: 'Lifestyle' }, + { id: 'photography', name: 'Photography' }, + ]} +/> +``` + +## Using A Custom Element For Options + +You can pass a custom element as [`optionText`](#optiontext) to have `` render each suggestion in a custom way. -**Tip**: React-admin's `` has only a capital A, while MUI's `` has a capital A and a capital C. Don't mix up the components! +`` will render the custom option element inside a [``](./useRecordContext.md), using the related choice as the `record` prop. You can use Field components there. + +However, as the underlying MUI `` component requires that the current selection is a string, you must also pass a function as the `inputText` prop. This function should return a text representation of the current selection. You should also pass a `matchSuggestion` function to filter the choices based on the current selection. + +```jsx +const choices = [ + { id: 123, first_name: 'Leo', last_name: 'Tolstoi', avatar:'/penguin' }, + { id: 456, first_name: 'Jane', last_name: 'Austen', avatar:'/panda' }, +]; +const OptionRenderer = () => { + const record = useRecordContext(); + return ( + + + {record.first_name} {record.last_name} + + ); +}; +const inputText = choice => `${choice.first_name} ${choice.last_name}`; +const matchSuggestion = (filter, choice) => { + return ( + choice.first_name.toLowerCase().includes(filter.toLowerCase()) + || choice.last_name.toLowerCase().includes(filter.toLowerCase()) + ); +}; + +} + inputText={inputText} + matchSuggestion={matchSuggestion} +/> +``` ## Creating New Choices @@ -270,6 +635,3 @@ const CreateTag = () => { ``` {% endraw %} -## `sx`: CSS API - -This component doesn't apply any custom styles on top of [MUI `` component](https://mui.com/components/autocomplete/). Refer to their documentation to know its CSS API. diff --git a/docs/AutocompleteInput.md b/docs/AutocompleteInput.md index e508b0f88b4..c733d330fe5 100644 --- a/docs/AutocompleteInput.md +++ b/docs/AutocompleteInput.md @@ -69,6 +69,8 @@ The form value for the source must be the selected value, e.g. | `setFilter` | Optional | `Function` | `null` | A callback to inform the `searchText` has changed and new `choices` can be retrieved based on this `searchText`. Signature `searchText => void`. This function is automatically set up when using `ReferenceInput`. | | `shouldRender Suggestions` | Optional | `Function` | `() => true` | A function that returns a `boolean` to determine whether or not suggestions are rendered. | | `suggestionLimit` | Optional | `number` | `null` | Limits the numbers of suggestions that are shown in the dropdown list | +| `translateChoice` | Optional | `boolean` | `true` | Whether the choices should be translated | + `` also accepts the [common input props](./Inputs.md#common-input-props). diff --git a/docs/img/autocomplete-array-input.gif b/docs/img/autocomplete-array-input.gif index 8cfb4302428..8cf8d5e0173 100644 Binary files a/docs/img/autocomplete-array-input.gif and b/docs/img/autocomplete-array-input.gif differ diff --git a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.stories.tsx b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.stories.tsx index 650f84a795e..429f813de95 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.stories.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; import { Admin } from 'react-admin'; import { RaRecord, Resource, required, useCreate } from 'ra-core'; +import polyglotI18nProvider from 'ra-i18n-polyglot'; +import englishMessages from 'ra-language-english'; import { createMemoryHistory } from 'history'; import { Dialog, @@ -11,7 +13,8 @@ import { Stack, } from '@mui/material'; -import { Edit } from '../detail'; +import { AdminContext } from '../AdminContext'; +import { Create, Edit } from '../detail'; import { SimpleForm } from '../form'; import { AutocompleteArrayInput } from './AutocompleteArrayInput'; import { ReferenceArrayInput } from './ReferenceArrayInput'; @@ -19,6 +22,30 @@ import { useCreateSuggestionContext } from './useSupportCreateSuggestion'; export default { title: 'ra-ui-materialui/input/AutocompleteArrayInput' }; +const i18nProvider = polyglotI18nProvider(() => englishMessages); + +export const Basic = () => ( + + + + + + + +); + const dataProvider = { getOne: (resource, params) => Promise.resolve({ @@ -65,7 +92,7 @@ const BookEdit = () => { ); }; -export const Basic = () => ( +export const InEdit = () => (