From c6fe23fb68c77c7071f211108f6fe6d4d34c29a2 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Tue, 13 Jul 2021 15:48:49 +0200 Subject: [PATCH 1/3] Add support for reordering items in SimpleFormIterator --- docs/Inputs.md | 1119 +++++++++++------ .../ra-core/src/i18n/TranslationMessages.ts | 2 + packages/ra-language-english/src/index.ts | 2 + packages/ra-language-french/src/index.ts | 2 + .../src/button/IconButtonWithTooltip.tsx | 51 + packages/ra-ui-materialui/src/button/index.ts | 2 + .../src/detail/editFieldTypes.tsx | 2 +- .../src/form/SimpleFormIterator.spec.tsx | 137 +- .../src/form/SimpleFormIterator.tsx | 270 ++-- packages/ra-ui-materialui/src/form/index.tsx | 6 +- .../src/input/ArrayInput.spec.tsx | 2 +- .../src/input/DateTimeInput.spec.tsx | 2 +- 12 files changed, 1047 insertions(+), 550 deletions(-) create mode 100644 packages/ra-ui-materialui/src/button/IconButtonWithTooltip.tsx diff --git a/docs/Inputs.md b/docs/Inputs.md index e992937b7c5..15511bd757b 100644 --- a/docs/Inputs.md +++ b/docs/Inputs.md @@ -1,6 +1,6 @@ --- layout: default -title: "Input Components" +title: 'Input Components' --- # Input Components @@ -9,17 +9,33 @@ An `Input` component displays an input, or a dropdown list, a list of radio butt ```jsx // in src/posts.js -import * as React from "react"; -import { Edit, SimpleForm, ReferenceInput, SelectInput, TextInput, required } from 'react-admin'; +import * as React from 'react'; +import { + Edit, + SimpleForm, + ReferenceInput, + SelectInput, + TextInput, + required, +} from 'react-admin'; -export const PostEdit = (props) => ( +export const PostEdit = props => ( } {...props}> - + - + @@ -30,15 +46,15 @@ export const PostEdit = (props) => ( All input components accept the following props: -| Prop | Required | Type | Default | Description | -| --------------- | -------- | ------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `source` | Required | `string` | - | Name of the entity property to use for the input value | +| Prop | Required | Type | Default | Description | +| --------------- | -------- | ------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `source` | Required | `string` | - | Name of the entity property to use for the input value | | `label` | Optional | `string` | - | Input label. In i18n apps, the label is passed to the `translate` function. Defaults to the humanized `source` when omitted. Set `label={false}` to hide the label. | -| `validate` | Optional | `Function` | `array` | - | Validation rules for the current property. See the [Validation Documentation](./CreateEdit.md#validation) for details. | -| `helperText` | Optional | `string` | - | Text to be displayed under the input | -| `fullWidth` | Optional | `boolean` | `false` | If `true`, the input will expand to fill the form width | -| `className` | Optional | `string` | - | Class name (usually generated by JSS) to customize the look and feel of the field element itself | -| `formClassName` | Optional | `string` | - | Class name to be applied to the container of the input (e.g. the `
` forming each row in ``) | +| `validate` | Optional | `Function` | `array` | - | Validation rules for the current property. See the [Validation Documentation](./CreateEdit.md#validation) for details. | +| `helperText` | Optional | `string` | - | Text to be displayed under the input | +| `fullWidth` | Optional | `boolean` | `false` | If `true`, the input will expand to fill the form width | +| `className` | Optional | `string` | - | Class name (usually generated by JSS) to customize the look and feel of the field element itself | +| `formClassName` | Optional | `string` | - | Class name to be applied to the container of the input (e.g. the `
` forming each row in ``) | ```jsx @@ -86,7 +102,7 @@ Then you can display a text input to edit the author first name as follows: ```jsx import { BooleanInput } from 'react-admin'; - +; ``` ![BooleanInput](./img/boolean-input.png) @@ -96,6 +112,7 @@ This input does not handle `null` values. You would need the `, }} -/> +/>; ``` + {% endraw %} ![CustomBooleanInputCheckIcon](./img/custom-switch-icon.png) @@ -118,7 +136,7 @@ Refer to [Material UI Switch documentation](https://material-ui.com/api/switch) ```jsx import { NullableBooleanInput } from 'react-admin'; - +; ``` ![NullableBooleanInput](./img/nullable-boolean-input.gif) @@ -134,7 +152,7 @@ englishMessages.ra.boolean.false = 'False label'; englishMessages.ra.boolean.true = 'True label'; const i18nProvider = polyglotI18nProvider(() => englishMessages, 'en'); - +; ``` Additionally, individual instances of `NullableBooleanInput` may be customized by setting the `nullLabel`, `falseLabel` and `trueLabel` properties. Values specified for those properties will be translated by react-admin. @@ -148,7 +166,7 @@ import { NullableBooleanInput } from 'react-admin'; nullLabel="Either" falseLabel="No" trueLabel="Yes" -/> +/>; ``` ![NullableBooleanInput](./img/nullable-boolean-input-null-label.png) @@ -174,7 +192,7 @@ The appearance of `` depends on the browser, and falls back to a text ```jsx import { DateInput } from 'react-admin'; - +; ``` `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -192,7 +210,7 @@ An input for editing dates with time. `` renders a standard brows ```jsx import { DateTimeInput } from 'react-admin'; - +; ``` `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -207,17 +225,17 @@ import { DateTimeInput } from 'react-admin'; #### Properties -| Prop | Required | Type | Default | Description | -| --------------- | -------- | --------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `accept` | Optional | `string | string[]` | - | Accepted file type(s), e. g. 'image/*,.pdf'. If left empty, all file types are accepted. Equivalent of the `accept` attribute of an ``. See [MDN input docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept) for syntax and examples. | -| `children` | Optional | `ReactNode` | - | Element used to display the preview of an image (cloned several times if the select accepts multiple files). | -| `minSize` | Optional | `number` | 0 | Minimum image size (in bytes), e.g. 5000 for 5KB | -| `maxSize` | Optional | `number` | `Infinity` | Maximum image size (in bytes), e.g. 5000000 for 5MB | -| `multiple` | Optional | `boolean` | `false` | Set to true if the input should accept a list of images, false if it should only accept one image | -| `labelSingle` | Optional | `string` | 'ra.input.image. upload_single' | Invite displayed in the drop zone if the input accepts one image | -| `labelMultiple` | Optional | `string` | 'ra.input.file. upload_multiple' | Invite displayed in the drop zone if the input accepts several images | -| `placeholder` | Optional | `string` | `ReactNode` | - | Invite displayed in the drop zone, overrides `labelSingle` and `labelMultiple` | -| `options` | Optional | `Object` | `{}` | Additional options passed to react-dropzone's `useDropzone()` hook. See [the react-dropzone source](https://github.com/react-dropzone/react-dropzone/blob/master/src/index.js) for details . | +| Prop | Required | Type | Default | Description | +| --------------- | -------- | --------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `accept` | Optional | `string | string[]` | - | Accepted file type(s), e. g. 'image/\*,.pdf'. If left empty, all file types are accepted. Equivalent of the `accept` attribute of an ``. See [MDN input docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept) for syntax and examples. | +| `children` | Optional | `ReactNode` | - | Element used to display the preview of an image (cloned several times if the select accepts multiple files). | +| `minSize` | Optional | `number` | 0 | Minimum image size (in bytes), e.g. 5000 for 5KB | +| `maxSize` | Optional | `number` | `Infinity` | Maximum image size (in bytes), e.g. 5000000 for 5MB | +| `multiple` | Optional | `boolean` | `false` | Set to true if the input should accept a list of images, false if it should only accept one image | +| `labelSingle` | Optional | `string` | 'ra.input.image. upload_single' | Invite displayed in the drop zone if the input accepts one image | +| `labelMultiple` | Optional | `string` | 'ra.input.file. upload_multiple' | Invite displayed in the drop zone if the input accepts several images | +| `placeholder` | Optional | `string` | `ReactNode` | - | Invite displayed in the drop zone, overrides `labelSingle` and `labelMultiple` | +| `options` | Optional | `Object` | `{}` | Additional options passed to react-dropzone's `useDropzone()` hook. See [the react-dropzone source](https://github.com/react-dropzone/react-dropzone/blob/master/src/index.js) for details . | `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -242,7 +260,12 @@ The `ImageInput` component accepts an `options` prop, allowing to set the [react If the default Dropzone label doesn't fit with your need, you can pass a `placeholder` prop to overwrite it. The value can be anything React can render (`PropTypes.node`): ```jsx -Drop your file here

}> +Drop your file here

} +>
``` @@ -298,12 +321,17 @@ Writing a custom preview component is quite straightforward: it's a standard [fi When receiving **new** files, `FileInput` will add a `rawFile` property to the object passed as the `record` prop of children. This `rawFile` is the [File](https://developer.mozilla.org/en-US/docs/Web/API/File) instance of the newly added file. This can be useful to display information about size or MIME type inside a custom field. -The `FileInput` component accepts an `options` prop into which you can pass all the [react-dropzone properties](https://react-dropzone.netlify.com/#proptypes). +The `FileInput` component accepts an `options` prop into which you can pass all the [react-dropzone properties](https://react-dropzone.netlify.com/#proptypes). If the default Dropzone label doesn't fit with your need, you can pass a `placeholder` prop to overwrite it. The value can be anything React can render (`PropTypes.node`): ```jsx -Drop your file here

}> +Drop your file here

} +>
``` @@ -354,7 +382,7 @@ It is necessary for numeric values because of a [known React bug](https://github ```jsx import { NumberInput } from 'react-admin'; - +; ``` #### Properties @@ -381,7 +409,7 @@ You can customize the `step` props (which defaults to "any"). For instance, to r ```jsx import { PasswordInput } from 'react-admin'; - +; ``` ![Password Input](./img/password-input.png) @@ -390,7 +418,7 @@ It is possible to change the default behavior and display the value by default v ```jsx import { PasswordInput } from 'react-admin'; - +; ``` ![Password Input (visible)](./img/password-input-visible.png) @@ -398,9 +426,14 @@ import { PasswordInput } from 'react-admin'; **Tip**: It is possible to set the [`autocomplete` attribute](https://developer.mozilla.org/fr/docs/Web/HTML/Attributs/autocomplete) by injecting an input props: {% raw %} + ```jsx - + ``` + {% endraw %} ### `` @@ -421,50 +454,57 @@ Then use it as a normal input component: ```jsx import RichTextInput from 'ra-input-rich-text'; - +; ``` You can customize the rich text editor toolbar using the `toolbar` attribute, as described on the [Quill official toolbar documentation](https://quilljs.com/docs/modules/toolbar/). ```jsx - + ``` If you need to add Quill `modules` or `themes`, you can do so by passing them in the `options` prop. {% raw %} + ```jsx ``` + {% endraw %} If you need more customization, you can access the quill object through the `configureQuill` callback that will be called just after its initialization. ```jsx -const configureQuill = quill => quill.getModule('toolbar').addHandler('bold', function (value) { - this.quill.format('bold', value) -}); +const configureQuill = quill => + quill.getModule('toolbar').addHandler('bold', function (value) { + this.quill.format('bold', value); + }); // ... - +; ``` `` also accepts the [common input props](./Inputs.md#common-input-props). -**Tip**: When used inside a material-ui `` (e.g in the default `` view), `` displays link tooltip as cut off when the user wants to add a hyperlink to a word located on the left side of the input. This is due to an incompatibility between material-ui's `` component and Quill's positioning algorithm for the link tooltip. +**Tip**: When used inside a material-ui `` (e.g in the default `` view), `` displays link tooltip as cut off when the user wants to add a hyperlink to a word located on the left side of the input. This is due to an incompatibility between material-ui's `` component and Quill's positioning algorithm for the link tooltip. To fix this problem, you should override the default card style, as follows: @@ -492,7 +532,7 @@ import { Edit, SimpleForm, TextInput } from 'react-admin'; ```jsx import { TextInput } from 'react-admin'; - +; ``` #### Properties @@ -524,7 +564,7 @@ You can make the `` component resettable using the `resettable` prop. ```jsx import { TextInput } from 'react-admin'; - +; ``` ![resettable TextInput](./img/resettable-text-input.gif) @@ -545,34 +585,37 @@ Set the `choices` attribute to determine the options list (with `id`, `name` tup ```jsx import { AutocompleteInput } from 'react-admin'; - +; ``` #### Properties -| Prop | Required | Type | Default | Description | -| ------------------------- | -------- | -------------- | ------------ | ------------------------------------ | -| `allowEmpty` | Optional | `boolean` | `false` | If `false` and the `searchText` typed did not match any suggestion, the `searchText` will revert to the current value when the field is blurred. If `true` and the `searchText` is set to `''` then the field will set the input value to `null`. | -| `clearAlwaysVisible` | Optional | `boolean` | `false` | When `resettable` is true, set this prop to `true` to have the Reset button visible even when the field is empty | -| `choices` | Required | `Object[]` | `-` | List of items to autosuggest | -| `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 | -| `emptyValue` | Optional | `any` | `''` | The value to use for the empty element | -| `emptyText` | Optional | `string` | `''` | The text to use for the empty element | -| `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 | -| `inputText` | Optional | `Function` | `-` | If `optionText` is a custom Component, this function is needed to determine the text displayed for the current selection. | -| `resettable` | Optional | `boolean` | `false` | Display a button to reset the text filter. Useful when using `` inside the list filters | -| `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 react-autosuggest component. Ex.`(value) => value.trim() > 2` | -| `suggestionLimit` | Optional | `number` | `null` | Limits the numbers of suggestions that are shown in the dropdown list | +| Prop | Required | Type | Default | Description | +| ------------------------- | -------- | --------------------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `allowEmpty` | Optional | `boolean` | `false` | If `false` and the `searchText` typed did not match any suggestion, the `searchText` will revert to the current value when the field is blurred. If `true` and the `searchText` is set to `''` then the field will set the input value to `null`. | +| `clearAlwaysVisible` | Optional | `boolean` | `false` | When `resettable` is true, set this prop to `true` to have the Reset button visible even when the field is empty | +| `choices` | Required | `Object[]` | `-` | List of items to autosuggest | +| `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 | +| `emptyValue` | Optional | `any` | `''` | The value to use for the empty element | +| `emptyText` | Optional | `string` | `''` | The text to use for the empty element | +| `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 | +| `inputText` | Optional | `Function` | `-` | If `optionText` is a custom Component, this function is needed to determine the text displayed for the current selection. | +| `resettable` | Optional | `boolean` | `false` | Display a button to reset the text filter. Useful when using `` inside the list filters | +| `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 react-autosuggest component. Ex.`(value) => value.trim() > 2` | +| `suggestionLimit` | Optional | `number` | `null` | Limits the numbers of suggestions that are shown in the dropdown list | `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -585,26 +628,35 @@ const choices = [ { _id: 123, full_name: 'Leo Tolstoi', sex: 'M' }, { _id: 456, full_name: 'Jane Austen', sex: 'F' }, ]; - +; ``` `optionText` also accepts a function, so you can shape the option text at will: ```jsx const choices = [ - { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, - { id: 456, first_name: 'Jane', last_name: 'Austen' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`; - +; ``` `optionText` also accepts a custom Component. However, as the underlying Autocomplete component requires that the current selection is a string, if you opt for a Component, you must pass a function as the `inputText` prop. This function should return text representation of the current selection: ```jsx const choices = [ - { id: 123, first_name: 'Leo', last_name: 'Tolstoi', avatar:'/pengouin' }, - { id: 456, first_name: 'Jane', last_name: 'Austen', avatar:'/panda' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi', avatar: '/pengouin' }, + { id: 456, first_name: 'Jane', last_name: 'Austen', avatar: '/panda' }, ]; const OptionRenderer = choice => ( @@ -618,15 +670,15 @@ const inputText = choice => `${choice.first_name} ${choice.last_name}`; choices={choices} optionText={} inputText={inputText} -/> +/>; ``` 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: 'M', name: 'myroot.gender.male' }, + { id: 'F', name: 'myroot.gender.female' }, ]; ``` @@ -634,7 +686,7 @@ However, in some cases (e.g. inside a ``), you may not want the In that case, set the `translateChoice` prop to `false`. ```jsx - + ``` If you want to limit the initial choices shown to the current value only, you can set the `limitChoicesToValue` prop. @@ -645,11 +697,16 @@ Ex. ` { return val.trim().le `` renders a [material-ui `` component](https://material-ui.com/api/text-field/). Use the `options` attribute to override any of the `` attributes: {% raw %} + ```jsx - + ``` + {% endraw %} **Tip**: If you want to populate the `choices` attribute with a list of related records, you should decorate `` with [``](#referenceinput), and leave the `choices` empty: @@ -659,21 +716,27 @@ import { AutocompleteInput, ReferenceInput } from 'react-admin'; - +; ``` Lastly, would you need to override the props of the suggestion's container (a `Popper` element), you can specify them using the `options.suggestionsContainerProps`. For example: {% raw %} + ```jsx - + ``` + {% endraw %} -**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 [``](#referenceinput) doesn't cover your need), you'll have to [write your own Input component](#writing-your-own-input-component) based on material-ui `` component. +**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 [``](#referenceinput) doesn't cover your need), you'll have to [write your own Input component](#writing-your-own-input-component) based on material-ui `` component. #### Creating New Choices @@ -682,10 +745,11 @@ The `` can allow users to create a new choice if either the ` Use the `onCreate` prop when you only require users to provide a simple string and a `prompt` is enough. You can return either the new choice directly or a Promise resolving to the new choice. {% raw %} + ```js import { AutocompleteInput, Create, SimpleForm, TextInput } from 'react-admin'; -const PostCreate = (props) => { +const PostCreate = props => { const categories = [ { name: 'Tech', id: 'tech' }, { name: 'Lifestyle', id: 'lifestyle' }, @@ -697,7 +761,10 @@ const PostCreate = (props) => { { const newCategoryName = prompt('Enter a new category'); - const newCategory = { id: newCategoryName.toLowerCase(), name: newCategoryName }; + const newCategory = { + id: newCategoryName.toLowerCase(), + name: newCategoryName, + }; categories.push(newCategory); return newCategory; }} @@ -707,13 +774,15 @@ const PostCreate = (props) => {
); -} +}; ``` + {% endraw %} Use the `create` prop when you want a more polished or complex UI. For example a Material UI `` asking for multiple fields because the choices are from a referenced resource. {% raw %} + ```js import { AutocompleteInput, @@ -721,7 +790,7 @@ import { ReferenceInput, SimpleForm, TextInput, - useCreateSuggestionContext + useCreateSuggestionContext, } from 'react-admin'; import { @@ -734,7 +803,7 @@ import { TextField, } from '@material-ui/core'; -const PostCreate = (props) => { +const PostCreate = props => { return ( @@ -745,14 +814,14 @@ const PostCreate = (props) => { ); -} +}; const CreateCategory = () => { const { filter, onCancel, onCreate } = useCreateSuggestionContext(); const [value, setValue] = React.useState(filter || ''); const [create] = useCreate('categories'); - const handleSubmit = (event) => { + const handleSubmit = event => { event.preventDefault(); create( { @@ -791,6 +860,7 @@ const CreateCategory = () => { ); }; ``` + {% endraw %} #### CSS API @@ -814,11 +884,14 @@ Set the `choices` attribute to determine the options (with `id`, `name` tuples): ```jsx import { RadioButtonGroupInput } from 'react-admin'; - +; ``` #### Properties @@ -843,54 +916,80 @@ const choices = [ { _id: 123, full_name: 'Leo Tolstoi', sex: 'M' }, { _id: 456, full_name: 'Jane Austen', sex: 'F' }, ]; - +; ``` `optionText` also accepts a function, so you can shape the option text at will: ```jsx const choices = [ - { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, - { id: 456, first_name: 'Jane', last_name: 'Austen' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { 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 cloned and receive 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' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; -const FullNameField = ({ record }) => {record.first_name} {record.last_name}; -}/> +const FullNameField = ({ record }) => ( + + {record.first_name} {record.last_name} + +); +} +/>; ``` 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: 'M', name: 'myroot.gender.male' }, + { id: 'F', name: 'myroot.gender.female' }, ]; ``` 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 - + ``` Lastly, use the `options` attribute if you want to override any of Material UI's `` attributes: {% raw %} + ```jsx - + ``` + {% endraw %} Refer to [Material UI RadioGroup documentation](https://material-ui.com/api/radio-group) for more details. @@ -902,7 +1001,7 @@ import { RadioButtonGroupInput, ReferenceInput } from 'react-admin'; - +; ``` #### CSS API @@ -925,28 +1024,31 @@ Set the `choices` attribute to determine the options (with `id`, `name` tuples): ```jsx import { SelectInput } from 'react-admin'; - +; ``` #### Properties -| Prop | Required | Type | Default | Description | -| ----------------- | -------- | -------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| `allowEmpty` | Optional | `boolean` | `false` | If true, the first option is an empty one | -| `choices` | Required | `Object[]` | - | List of items to show as options | -| `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 | -| `emptyText` | Optional | `string` | '' | The text to display for the empty option | -| `onCreate` | Optional | `Function` | `-` | A function called with the current filter value when users choose to create a new choice. | -| `options` | Optional | `Object` | - | Props to pass to the underlying `` element | -| `optionText` | Optional | `string` | `Function` | `name` | Field name of record to display in the suggestion item or function which accepts the current record as argument (`record => {string}`) | -| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | -| `resettable` | Optional | `boolean` | `false` | If `true`, display a button to reset the changes in this input value | -| `translateChoice` | Optional | `boolean` | `true` | Whether the choices should be translated | +| Prop | Required | Type | Default | Description | +| ----------------- | -------- | -------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | +| `allowEmpty` | Optional | `boolean` | `false` | If true, the first option is an empty one | +| `choices` | Required | `Object[]` | - | List of items to show as options | +| `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 | +| `emptyText` | Optional | `string` | '' | The text to display for the empty option | +| `onCreate` | Optional | `Function` | `-` | A function called with the current filter value when users choose to create a new choice. | +| `options` | Optional | `Object` | - | Props to pass to the underlying `` element | +| `optionText` | Optional | `string` | `Function` | `name` | Field name of record to display in the suggestion item or function which accepts the current record as argument (`record => {string}`) | +| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | +| `resettable` | Optional | `boolean` | `false` | If `true`, display a button to reset the changes in this input value | +| `translateChoice` | Optional | `boolean` | `true` | Whether the choices should be translated | `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -959,54 +1061,76 @@ const choices = [ { _id: 123, full_name: 'Leo Tolstoi', sex: 'M' }, { _id: 456, full_name: 'Jane Austen', sex: 'F' }, ]; - +; ``` `optionText` also accepts a function, so you can shape the option text at will: ```jsx const choices = [ - { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, - { id: 456, first_name: 'Jane', last_name: 'Austen' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { 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 cloned and receive 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' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; -const FullNameField = ({ record }) => {record.first_name} {record.last_name}; -}/> +const FullNameField = ({ record }) => ( + + {record.first_name} {record.last_name} + +); +} +/>; ``` Enabling the `allowEmpty` props adds an empty choice (with a default `''` value, which you can overwrite with the `emptyValue` prop) on top of the options. You can furthermore customize the `MenuItem` for the empty choice by using the `emptyText` prop, which can receive either a string or a React Element, which doesn't receive any props. ```jsx - + ``` 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: 'M', name: 'myroot.gender.male' }, + { id: 'F', name: 'myroot.gender.female' }, ]; ``` However, in some cases, you may not want the choice to be translated. In that case, set the `translateChoice` prop to `false`. ```jsx - + ``` Note that `translateChoice` is set to `false` when `` is a child of ``. @@ -1014,11 +1138,16 @@ Note that `translateChoice` is set to `false` when `` is a child of Lastly, use the `options` attribute if you want to override any of Material UI's `` attributes: {% raw %} + ```jsx - + ``` + {% endraw %} Refer to [Material UI Select documentation](https://material-ui.com/api/select) for more details. @@ -1030,7 +1159,7 @@ import { SelectInput, ReferenceInput } from 'react-admin'; - +; ``` If, instead of showing choices as a dropdown list, you prefer to display them as a list of radio buttons, try the [``](#radiobuttongroupinput). And if the list is too big, prefer the [``](#autocompleteinput). @@ -1047,7 +1176,12 @@ const choices = [ { _id: 456, full_name: 'Jane Austen', sex: 'F' }, { _id: 1, full_name: 'System Administrator', sex: 'F', disabled: true }, ]; - +; ``` You can use a custom field name by setting `disableValue` prop: @@ -1058,7 +1192,13 @@ const choices = [ { _id: 456, full_name: 'Jane Austen', sex: 'F' }, { _id: 987, full_name: 'Jack Harden', sex: 'M', not_available: true }, ]; - +; ``` #### Creating New Choices @@ -1068,10 +1208,11 @@ The `` can allow users to create a new choice if either the `create Use the `onCreate` prop when you only require users to provide a simple string and a `prompt` is enough. You can return either the new choice directly or a Promise resolving to the new choice. {% raw %} + ```js import { SelectInput, Create, SimpleForm, TextInput } from 'react-admin'; -const PostCreate = (props) => { +const PostCreate = props => { const categories = [ { name: 'Tech', id: 'tech' }, { name: 'Lifestyle', id: 'lifestyle' }, @@ -1083,7 +1224,10 @@ const PostCreate = (props) => { { const newCategoryName = prompt('Enter a new category'); - const newCategory = { id: newCategoryName.toLowerCase(), name: newCategoryName }; + const newCategory = { + id: newCategoryName.toLowerCase(), + name: newCategoryName, + }; categories.push(newCategory); return newCategory; }} @@ -1093,13 +1237,15 @@ const PostCreate = (props) => { ); -} +}; ``` + {% endraw %} Use the `create` prop when you want a more polished or complex UI. For example a Material UI `` asking for multiple fields because the choices are from a referenced resource. {% raw %} + ```js import { SelectInput, @@ -1107,7 +1253,7 @@ import { ReferenceInput, SimpleForm, TextInput, - useCreateSuggestionContext + useCreateSuggestionContext, } from 'react-admin'; import { @@ -1120,7 +1266,7 @@ import { TextField, } from '@material-ui/core'; -const PostCreate = (props) => { +const PostCreate = props => { return ( @@ -1131,14 +1277,14 @@ const PostCreate = (props) => { ); -} +}; const CreateCategory = () => { const { filter, onCancel, onCreate } = useCreateSuggestionContext(); const [value, setValue] = React.useState(filter || ''); const [create] = useCreate('categories'); - const handleSubmit = (event) => { + const handleSubmit = event => { event.preventDefault(); create( { @@ -1177,6 +1323,7 @@ const CreateCategory = () => { ); }; ``` + {% endraw %} #### CSS API @@ -1196,14 +1343,19 @@ To edit arrays of data embedded inside a record, `` creates a list o ![ArrayInput](./img/array-input.gif) ```jsx -import { ArrayInput, SimpleFormIterator, DateInput, TextInput } from 'react-admin'; +import { + ArrayInput, + SimpleFormIterator, + DateInput, + TextInput, +} from 'react-admin'; - +; ``` `` allows editing of embedded arrays, like the `authors` field in the following `post` record: @@ -1220,7 +1372,7 @@ import { ArrayInput, SimpleFormIterator, DateInput, TextInput } from 'react-admi "user_id": 456, "url": "co_writer", } - ] + ] } ``` @@ -1228,52 +1380,77 @@ import { ArrayInput, SimpleFormIterator, DateInput, TextInput } from 'react-admi `` expects a single child, which must be a *form iterator* component. A form iterator is a component accepting a `fields` object as passed by [react-final-form-array](https://github.com/final-form/react-final-form-arrays#fieldarrayrenderprops), and defining a layout for an array of fields. For instance, the `` component 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 (a backlink in this example). -You can pass `disableAdd` and `disableRemove` as props of `SimpleFormIterator`, to disable `ADD` and `REMOVE` button respectively. Default value of both is `false`. +You can pass `disableAdd`, `disableRemove` and `disableReordering` as props of `SimpleFormIterator`, to disable `ADD`, `REMOVE` and reordering buttons respectively. They are all enabled by default. ```jsx -import { ArrayInput, SimpleFormIterator, DateInput, TextInput } from 'react-admin'; +import { + ArrayInput, + SimpleFormIterator, + DateInput, + TextInput, +} from 'react-admin'; - + - +; ``` -You can also use `addButton` and `removeButton` props to pass your custom add and remove buttons to `SimpleFormIterator`. +You can also use `addButton`, `removeButton` or `reOrderButtons` props to pass your custom add and remove buttons to `SimpleFormIterator`. ```jsx -import { ArrayInput, SimpleFormIterator, DateInput, TextInput } from 'react-admin'; +import { + ArrayInput, + SimpleFormIterator, + DateInput, + TextInput, +} from 'react-admin'; - } removeButton={}> + } + removeButton={} + reOrderButtons={} + > - +; ``` Furthermore, if you want to customize the label displayed for each item, you can pass a function to `` via the `getItemLabel` prop. ```jsx -import { ArrayInput, SimpleFormIterator, DateInput, TextInput } from 'react-admin'; +import { + ArrayInput, + SimpleFormIterator, + DateInput, + TextInput, +} from 'react-admin'; - `${index + 1}. link`}> + `${index + 1}. link`}> - +; ``` **Note**: `` only accepts `Input` components as children. If you want to use some `Fields` instead, you have to use a `` to get the correct source, as follows: ```jsx -import { ArrayInput, SimpleFormIterator, DateInput, TextInput, FormDataConsumer } from 'react-admin'; +import { + ArrayInput, + SimpleFormIterator, + DateInput, + TextInput, + FormDataConsumer, +} from 'react-admin'; - + {({ getSource, scopedFormData }) => { @@ -1286,10 +1463,10 @@ import { ArrayInput, SimpleFormIterator, DateInput, TextInput, FormDataConsumer }} - +; ``` -`` also accepts the [common input props](./Inputs.md#common-input-props) (except `format` and `parse`). +`` also accepts the [common input props](./Inputs.md#common-input-props) (except `format` and `parse`). **Important**: Note that asynchronous validators are not supported on the `` component due to a limitation of [react-final-form-arrays](https://github.com/final-form/react-final-form-arrays). @@ -1305,32 +1482,35 @@ Set the `choices` attribute to determine the options list (with `id`, `name` tup ```jsx import { AutocompleteArrayInput } from 'react-admin'; - +; ``` #### Properties -| Prop | Required | Type | Default | Description | -| ------------------------- | -------- | -------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `allowEmpty` | Optional | `boolean` | `false` | If `true`, the first option is an empty one | -| `allowDuplicates` | Optional | `boolean` | `false` | If `true`, the options can be selected several times | -| `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. | -| `choices` | Required | `Object[]` | - | List of items to autosuggest | -| `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. | -| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | -| `optionText` | Optional | `string` | `Function` | `name` | Field name of record to display in the suggestion item or function which accepts the current record as argument (`record => {string}`) | -| `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 react-autosuggest component. Ex.`(value) => value.trim() > 2` | -| `source` | Required | `string` | - | Name of field to edit, its type should match the type retrieved from `optionValue` | -| `suggestionLimit` | Optional | `number` | `null` | Limits the numbers of suggestions that are shown in the dropdown list | +| Prop | Required | Type | Default | Description | +| ------------------------- | -------- | -------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `allowEmpty` | Optional | `boolean` | `false` | If `true`, the first option is an empty one | +| `allowDuplicates` | Optional | `boolean` | `false` | If `true`, the options can be selected several times | +| `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. | +| `choices` | Required | `Object[]` | - | List of items to autosuggest | +| `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. | +| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | +| `optionText` | Optional | `string` | `Function` | `name` | Field name of record to display in the suggestion item or function which accepts the current record as argument (`record => {string}`) | +| `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 react-autosuggest component. Ex.`(value) => value.trim() > 2` | +| `source` | Required | `string` | - | Name of field to edit, its type should match the type retrieved from `optionValue` | +| `suggestionLimit` | Optional | `number` | `null` | Limits the numbers of suggestions that are shown in the dropdown list | `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -1343,33 +1523,46 @@ const choices = [ { _id: 123, full_name: 'Leo Tolstoi', sex: 'M' }, { _id: 456, full_name: 'Jane Austen', sex: 'F' }, ]; - +; ``` `optionText` also accepts a function, so you can shape the option text at will: ```jsx const choices = [ - { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, - { id: 456, first_name: 'Jane', last_name: 'Austen' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`; - +; ``` 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: 'M', name: 'myroot.gender.male' }, + { id: 'F', name: 'myroot.gender.female' }, ]; ``` 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). @@ -1378,11 +1571,16 @@ Ex. ` { return val.trim Lastly, `` renders a [material-ui `` component](https://material-ui.com/api/text-field/). Use the `options` attribute to override any of the `` attributes: {% raw %} + ```jsx - + ``` + {% endraw %} **Tip**: Like many other inputs, `` accept a `fullWidth` prop. @@ -1394,21 +1592,27 @@ import { AutocompleteArrayInput, ReferenceArrayInput } from 'react-admin'; - +; ``` If you need to override the props of the suggestion's container (a `Popper` element), you can specify them using the `options.suggestionsContainerProps`. For example: {% raw %} + ```jsx - + ``` + {% endraw %} -**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) doesn't cover your need), you'll have to [write your own Input component](#writing-your-own-input-component) based on [material-ui-chip-input](https://github.com/TeamWertarbyte/material-ui-chip-input). +**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) doesn't cover your need), you'll have to [write your own Input component](#writing-your-own-input-component) based on [material-ui-chip-input](https://github.com/TeamWertarbyte/material-ui-chip-input). **Tip**: React-admin's `` has only a capital A, while material-ui's `` has a capital A and a capital C. Don't mix up the components! @@ -1419,10 +1623,16 @@ The `` can allow users to create a new choice if either Use the `onCreate` prop when you only require users to provide a simple string and a `prompt` is enough. 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'; +import { + AutocompleteArrayInput, + Create, + SimpleForm, + TextInput, +} from 'react-admin'; -const PostCreate = (props) => { +const PostCreate = props => { const tags = [ { name: 'Tech', id: 'tech' }, { name: 'Lifestyle', id: 'lifestyle' }, @@ -1434,7 +1644,10 @@ const PostCreate = (props) => { { const newTagName = prompt('Enter a new tag'); - const newTag = { id: newTagName.toLowerCase(), name: newTagName }; + const newTag = { + id: newTagName.toLowerCase(), + name: newTagName, + }; categories.push(newTag); return newTag; }} @@ -1444,13 +1657,15 @@ const PostCreate = (props) => { ); -} +}; ``` + {% endraw %} Use the `create` prop when you want a more polished or complex UI. For example a Material UI `` asking for multiple fields because the choices are from a referenced resource. {% raw %} + ```js import { AutocompleteArrayInput, @@ -1458,7 +1673,7 @@ import { ReferenceArrayInput, SimpleForm, TextInput, - useCreateSuggestionContext + useCreateSuggestionContext, } from 'react-admin'; import { @@ -1471,7 +1686,7 @@ import { TextField, } from '@material-ui/core'; -const PostCreate = (props) => { +const PostCreate = props => { return ( @@ -1482,14 +1697,14 @@ const PostCreate = (props) => { ); -} +}; const CreateTag = () => { const { filter, onCancel, onCreate } = useCreateSuggestionContext(); const [value, setValue] = React.useState(filter || ''); const [create] = useCreate('tags'); - const handleSubmit = (event) => { + const handleSubmit = event => { event.preventDefault(); create( { @@ -1528,6 +1743,7 @@ const CreateTag = () => { ); }; ``` + {% endraw %} #### CSS API @@ -1556,17 +1772,20 @@ Set the `choices` attribute to determine the options (with `id`, `name` tuples): ```jsx import { CheckboxGroupInput } from 'react-admin'; - +; ``` #### Properties | Prop | Required | Type | Default | Description | -| ------------- | -------- | -------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| ------------- | -------- | -------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `choices` | Required | `Object[]` | - | List of choices | | `optionText` | Optional | `string` | `Function` | `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 | @@ -1585,29 +1804,46 @@ const choices = [ { _id: 123, full_name: 'Leo Tolstoi', sex: 'M' }, { _id: 456, full_name: 'Jane Austen', sex: 'F' }, ]; - +; ``` `optionText` also accepts a function, so you can shape the option text at will: ```jsx const choices = [ - { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, - { id: 456, first_name: 'Jane', last_name: 'Austen' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { 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 cloned and receive 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' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; -const FullNameField = ({ record }) => {record.first_name} {record.last_name}; -}/> +const FullNameField = ({ record }) => ( + + {record.first_name} {record.last_name} + +); +} +/>; ``` The choices are translated by default, so you can use translation identifiers as choices: @@ -1623,20 +1859,25 @@ const choices = [ 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 - + ``` Lastly, use the `options` attribute if you want to override any of Material UI's `` attributes: {% raw %} + ```jsx import { FavoriteBorder, Favorite } from '@material-ui/icons'; -, - checkedIcon: -}} /> +, + checkedIcon: , + }} +/>; ``` + {% endraw %} #### CSS API @@ -1660,7 +1901,7 @@ import { DualListInput } from '@react-admin/ra-relationships'; - +; ``` Check [the `ra-relationships` documentation](https://marmelab.com/ra-enterprise/modules/ra-relationships) for more details. @@ -1673,19 +1914,19 @@ To let users choose several values in a list using a dropdown, use `` element | -| `optionText` | Optional | `string` | `Function` | `name` | Field name of record to display in the suggestion item or function which accepts the current record as argument (`record => {string}`) | -| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | -| `resettable` | Optional | `boolean` | `false` | If `true`, display a button to reset the changes in this input value | -| `translateChoice` | Optional | `boolean` | `true` | Whether the choices should be translated | +| Prop | Required | Type | Default | Description | +| ----------------- | -------- | -------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | +| `allowEmpty` | Optional | `boolean` | `false` | If true, the first option is an empty one | +| `choices` | Required | `Object[]` | - | List of items to show as options | +| `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 | +| `emptyText` | Optional | `string` | '' | The text to display for the empty option | +| `onCreate` | Optional | `Function` | `-` | A function called with the current filter value when users choose to create a new choice. | +| `options` | Optional | `Object` | - | Props to pass to the underlying `` element | +| `optionText` | Optional | `string` | `Function` | `name` | Field name of record to display in the suggestion item or function which accepts the current record as argument (`record => {string}`) | +| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | +| `resettable` | Optional | `boolean` | `false` | If `true`, display a button to reset the changes in this input value | +| `translateChoice` | Optional | `boolean` | `true` | Whether the choices should be translated | `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -1696,13 +1937,17 @@ Set the `choices` attribute to determine the options (with `id`, `name` tuples): ```jsx import { SelectArrayInput } from 'react-admin'; - +; ``` You can also customize the properties to use for the option name and value, @@ -1710,35 +1955,44 @@ thanks to the `optionText` and `optionValue` attributes. ```jsx const choices = [ - { _id: '1', name: 'Book', plural_name: 'Books' }, - { _id: '2', name: 'Video', plural_name: 'Videos' }, - { _id: '3', name: 'Audio', plural_name: 'Audios' }, + { _id: '1', name: 'Book', plural_name: 'Books' }, + { _id: '2', name: 'Video', plural_name: 'Videos' }, + { _id: '3', name: 'Audio', plural_name: 'Audios' }, ]; - +; ``` `optionText` also accepts a function, so you can shape the option text at will: ```jsx const choices = [ - { id: '1', name: 'Book', quantity: 23 }, - { id: '2', name: 'Video', quantity: 56 }, - { id: '3', name: 'Audio', quantity: 12 }, + { id: '1', name: 'Book', quantity: 23 }, + { id: '2', name: 'Video', quantity: 56 }, + { id: '3', name: 'Audio', quantity: 12 }, ]; const optionRenderer = choice => `${choice.name} (${choice.quantity})`; - +; ``` The choices are translated by default, so you can use translation identifiers as choices: ```js const choices = [ - { id: 'books', name: 'myroot.category.books' }, - { id: 'sport', name: 'myroot.category.sport' }, + { id: 'books', name: 'myroot.category.books' }, + { id: 'sport', name: 'myroot.category.sport' }, ]; ``` -You can render any item as disabled by setting its `disabled` property to `true`: +You can render any item as disabled by setting its `disabled` property to `true`: ```jsx const choices = [ @@ -1746,10 +2000,15 @@ const choices = [ { _id: 456, full_name: 'Jane Austen', sex: 'F' }, { _id: 1, full_name: 'System Administrator', sex: 'F', disabled: true }, ]; - +; ``` -You can use a custom field name by setting the `disableValue` prop: +You can use a custom field name by setting the `disableValue` prop: ```jsx const choices = [ @@ -1757,15 +2016,23 @@ const choices = [ { _id: 456, full_name: 'Jane Austen', sex: 'F' }, { _id: 987, full_name: 'Jack Harden', sex: 'M', not_available: true }, ]; - +; ``` Lastly, use the `options` attribute if you want to override any of the ` - + + ``` @@ -2598,9 +2894,19 @@ const ItemEdit = (props) => ( ```jsx const LatLongInput = () => ( - +   - + ); ``` @@ -2617,9 +2923,19 @@ import { Labeled } from 'react-admin'; const LatLngInput = () => ( - +   - + ); @@ -2631,8 +2947,8 @@ Now the component will render with a label: ```html - - + + ``` @@ -2648,7 +2964,7 @@ import { useField } from 'react-final-form'; const BoundedTextField = ({ name, label }) => { const { input: { onChange }, - meta: { touched, error } + meta: { touched, error }, } = useField(name); return ( { const { input: { name, onChange, ...rest }, meta: { touched, error }, - isRequired + isRequired, } = useInput(props); return ( @@ -2723,13 +3039,23 @@ const BoundedTextField = props => { ); }; const LatLngInput = props => { - const {source, ...rest} = props; + const { source, ...rest } = props; return ( - +   - + ); }; @@ -2746,14 +3072,11 @@ import { useInput } from 'react-admin'; const SexInput = props => { const { input, - meta: { touched, error } + meta: { touched, error }, } = useInput(props); return ( - Male Female @@ -2765,16 +3088,20 @@ export default SexInput; **Tip**: `useInput` accepts all arguments that you can pass to `useField`. That means that components using `useInput` accept props like [format](https://final-form.org/docs/react-final-form/types/FieldProps#format) and [parse](https://final-form.org/docs/react-final-form/types/FieldProps#parse), to convert values from the form to the input, and vice-versa: ```jsx -const parse = value => {/* ... */}; -const format = value => {/* ... */}; +const parse = value => { + /* ... */ +}; +const format = value => { + /* ... */ +}; const PersonEdit = props => ( formValue === 0 ? 'M' : 'F'} - parse={inputValue => inputValue === 'M' ? 0 : 1} + format={formValue => (formValue === 0 ? 'M' : 'F')} + parse={inputValue => (inputValue === 'M' ? 0 : 1)} /> @@ -2791,4 +3118,4 @@ You can find components for react-admin in third-party repositories. - [@bb-tech/ra-components](https://github.com/bigbasket/ra-components): `JsonInput` which allows only valid JSON as input, `JsonField` to view JSON properly on show card and `TrimField` to trim the fields while showing in `Datagrid` in `List` component. - [@react-page/react-admin](https://react-page.github.io/docs/#/integration-react-admin): ReactPage is a rich content editor and can comes with a ready-to-use React-admin input component. [check out the demo](https://react-page.github.io/examples/reactadmin) -- **DEPRECATED V3** [LoicMahieu/aor-tinymce-input](https://github.com/LoicMahieu/aor-tinymce-input): a TinyMCE component, useful for editing HTML +- **DEPRECATED V3** [LoicMahieu/aor-tinymce-input](https://github.com/LoicMahieu/aor-tinymce-input): a TinyMCE component, useful for editing HTML diff --git a/packages/ra-core/src/i18n/TranslationMessages.ts b/packages/ra-core/src/i18n/TranslationMessages.ts index 52808d687a8..8c41c786228 100644 --- a/packages/ra-core/src/i18n/TranslationMessages.ts +++ b/packages/ra-core/src/i18n/TranslationMessages.ts @@ -35,6 +35,8 @@ export interface TranslationMessages extends StringMap { open_menu: string; close_menu: string; update: string; + move_up: string; + move_down: string; }; boolean: { [key: string]: StringMap | string; diff --git a/packages/ra-language-english/src/index.ts b/packages/ra-language-english/src/index.ts index 62b3003e4c1..f1a526dfaaa 100644 --- a/packages/ra-language-english/src/index.ts +++ b/packages/ra-language-english/src/index.ts @@ -31,6 +31,8 @@ const englishMessages: TranslationMessages = { open_menu: 'Open menu', close_menu: 'Close menu', update: 'Update', + move_up: 'Move up', + move_down: 'Move down', }, boolean: { true: 'Yes', diff --git a/packages/ra-language-french/src/index.ts b/packages/ra-language-french/src/index.ts index ec1c8a15c87..c8774214479 100644 --- a/packages/ra-language-french/src/index.ts +++ b/packages/ra-language-french/src/index.ts @@ -32,6 +32,8 @@ const frenchMessages: TranslationMessages = { open_menu: 'Ouvrir le menu', close_menu: 'Fermer le menu', update: 'Modifier', + move_up: 'Déplacer vers le haut', + move_down: 'Déplacer vers le bas', }, boolean: { true: 'Oui', diff --git a/packages/ra-ui-materialui/src/button/IconButtonWithTooltip.tsx b/packages/ra-ui-materialui/src/button/IconButtonWithTooltip.tsx new file mode 100644 index 00000000000..9629da1a973 --- /dev/null +++ b/packages/ra-ui-materialui/src/button/IconButtonWithTooltip.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { MouseEvent } from 'react'; +import { IconButton, IconButtonProps, Tooltip } from '@material-ui/core'; +import { useTranslate } from 'ra-core'; + +/** + * An IconButton with a tooltip which ensures the tooltip is closed on click to avoid ghost tooltips + * when the button position changes. + */ +export const IconButtonWithTooltip = ({ + label, + onClick, + ...props +}: IconButtonWithTooltipProps) => { + const translate = useTranslate(); + const [open, setOpen] = React.useState(false); + + const handleClose = () => { + setOpen(false); + }; + + const handleOpen = () => { + setOpen(true); + }; + + const translatedLabel = translate(label, { _: label }); + + const handleClick = (event: MouseEvent) => { + handleClose(); + onClick(event); + }; + + return ( + + + + ); +}; + +export interface IconButtonWithTooltipProps extends IconButtonProps { + label: string; +} diff --git a/packages/ra-ui-materialui/src/button/index.ts b/packages/ra-ui-materialui/src/button/index.ts index 93cc58b369f..1489acf5bda 100644 --- a/packages/ra-ui-materialui/src/button/index.ts +++ b/packages/ra-ui-materialui/src/button/index.ts @@ -26,6 +26,8 @@ export * from './DeleteButton'; export * from './DeleteWithConfirmButton'; export * from './DeleteWithUndoButton'; +export * from './IconButtonWithTooltip'; + export type { BulkDeleteButtonProps, BulkDeleteWithConfirmButtonProps, diff --git a/packages/ra-ui-materialui/src/detail/editFieldTypes.tsx b/packages/ra-ui-materialui/src/detail/editFieldTypes.tsx index 9af5b891529..5fb4fc2a034 100644 --- a/packages/ra-ui-materialui/src/detail/editFieldTypes.tsx +++ b/packages/ra-ui-materialui/src/detail/editFieldTypes.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { ReactNode, ReactElement } from 'react'; import SimpleForm from '../form/SimpleForm'; -import SimpleFormIterator from '../form/SimpleFormIterator'; +import { SimpleFormIterator } from '../form/SimpleFormIterator'; import ArrayInput from '../input/ArrayInput'; import BooleanInput from '../input/BooleanInput'; import DateInput from '../input/DateInput'; diff --git a/packages/ra-ui-materialui/src/form/SimpleFormIterator.spec.tsx b/packages/ra-ui-materialui/src/form/SimpleFormIterator.spec.tsx index 0b66aad6797..415fe0d1db4 100644 --- a/packages/ra-ui-materialui/src/form/SimpleFormIterator.spec.tsx +++ b/packages/ra-ui-materialui/src/form/SimpleFormIterator.spec.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import { ArrayInput } from '../input'; import TextInput from '../input/TextInput'; import SimpleForm from './SimpleForm'; -import SimpleFormIterator from './SimpleFormIterator'; +import { SimpleFormIterator } from './SimpleFormIterator'; const theme = createMuiTheme(); @@ -266,10 +266,10 @@ describe('', () => { const inputElements = queryAllByLabelText( 'resources.undefined.fields.email' - ); + ) as HTMLInputElement[]; expect( - inputElements.map((inputElement: HTMLInputElement) => ({ + inputElements.map(inputElement => ({ email: inputElement.value, })) ).toEqual([{ email: '' }, { email: '' }]); @@ -305,13 +305,13 @@ describe('', () => { expect(inputElements.length).toBe(1); }); - const inputElements = queryAllByLabelText('CustomLabel'); + const inputElements = queryAllByLabelText( + 'CustomLabel' + ) as HTMLInputElement[]; - expect( - inputElements.map( - (inputElement: HTMLInputElement) => inputElement.value - ) - ).toEqual(['']); + expect(inputElements.map(inputElement => inputElement.value)).toEqual([ + '', + ]); expect(queryAllByText('ra.action.remove').length).toBe(1); }); @@ -348,13 +348,13 @@ describe('', () => { expect(inputElements.length).toBe(1); }); - const inputElements = queryAllByLabelText('CustomLabel'); + const inputElements = queryAllByLabelText( + 'CustomLabel' + ) as HTMLInputElement[]; - expect( - inputElements.map( - (inputElement: HTMLInputElement) => inputElement.value - ) - ).toEqual(['5']); + expect(inputElements.map(inputElement => inputElement.value)).toEqual([ + '5', + ]); expect(queryAllByText('ra.action.remove').length).toBe(1); }); @@ -380,10 +380,10 @@ describe('', () => { const inputElements = queryAllByLabelText( 'resources.undefined.fields.email' - ); + ) as HTMLInputElement[]; expect( - inputElements.map((inputElement: HTMLInputElement) => ({ + inputElements.map(inputElement => ({ email: inputElement.value, })) ).toEqual(emails); @@ -397,18 +397,78 @@ describe('', () => { await waitFor(() => { const inputElements = queryAllByLabelText( 'resources.undefined.fields.email' - ); + ) as HTMLInputElement[]; expect( - inputElements.map((inputElement: HTMLInputElement) => ({ + inputElements.map(inputElement => ({ email: inputElement.value, })) ).toEqual([{ email: 'bar@foo.com' }]); }); }); + it('should reorder children on reorder buttons click', async () => { + const emails = [{ email: 'foo@bar.com' }, { email: 'bar@foo.com' }]; + + const { queryAllByLabelText } = renderWithRedux( + + + + + + + + + + + + + + ); + + const inputElements = queryAllByLabelText( + 'resources.undefined.fields.email' + ) as HTMLInputElement[]; + + expect( + inputElements.map(inputElement => ({ + email: inputElement.value, + })) + ).toEqual(emails); + + const moveDownFirstButton = queryAllByLabelText('ra.action.move_down'); + + fireEvent.click(moveDownFirstButton[0]); + await waitFor(() => { + const inputElements = queryAllByLabelText( + 'resources.undefined.fields.email' + ) as HTMLInputElement[]; + + expect( + inputElements.map(inputElement => ({ + email: inputElement.value, + })) + ).toEqual([{ email: 'bar@foo.com' }, { email: 'foo@bar.com' }]); + }); + + const moveUpButton = queryAllByLabelText('ra.action.move_up'); + + fireEvent.click(moveUpButton[1]); + await waitFor(() => { + const inputElements = queryAllByLabelText( + 'resources.undefined.fields.email' + ) as HTMLInputElement[]; + + expect( + inputElements.map(inputElement => ({ + email: inputElement.value, + })) + ).toEqual([{ email: 'foo@bar.com' }, { email: 'bar@foo.com' }]); + }); + }); + it('should not display the default add button if a custom add button is passed', () => { - const { queryAllByText } = renderWithRedux( + const { getByText, queryAllByText } = renderWithRedux( @@ -423,11 +483,13 @@ describe('', () => { ); + expect(queryAllByText('ra.action.add').length).toBe(0); + expect(getByText('Custom Add Button')).not.toBeNull(); }); it('should not display the default remove button if a custom remove button is passed', () => { - const { queryAllByText } = renderWithRedux( + const { getByText, queryAllByText } = renderWithRedux( @@ -450,30 +512,11 @@ describe('', () => { ); expect(queryAllByText('ra.action.remove').length).toBe(0); + expect(getByText('Custom Remove Button')).not.toBeNull(); }); - it('should display the custom add button', () => { - const { getByText } = renderWithRedux( - - - - - Custom Add Button} - > - - - - - - - ); - - expect(getByText('Custom Add Button')).not.toBeNull(); - }); - - it('should display the custom remove button', () => { - const { getByText } = renderWithRedux( + it('should not display the default reorder element if a custom reorder element is passed', () => { + const { getByText, queryAllByLabelText } = renderWithRedux( @@ -482,8 +525,8 @@ describe('', () => { > Custom Remove Button + reOrderButtons={ + } > @@ -495,7 +538,9 @@ describe('', () => { ); - expect(getByText('Custom Remove Button')).not.toBeNull(); + expect(queryAllByLabelText('ra.action.move_up').length).toBe(0); + expect(queryAllByLabelText('ra.action.move_down').length).toBe(0); + expect(getByText('Custom reorder Button')).not.toBeNull(); }); it('should display custom row label', () => { diff --git a/packages/ra-ui-materialui/src/form/SimpleFormIterator.tsx b/packages/ra-ui-materialui/src/form/SimpleFormIterator.tsx index f6f9a8a5051..f699719306e 100644 --- a/packages/ra-ui-materialui/src/form/SimpleFormIterator.tsx +++ b/packages/ra-ui-materialui/src/form/SimpleFormIterator.tsx @@ -1,104 +1,36 @@ -import Button from '@material-ui/core/Button'; -import FormHelperText from '@material-ui/core/FormHelperText'; -import { makeStyles } from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; -import AddIcon from '@material-ui/icons/AddCircleOutline'; -import CloseIcon from '@material-ui/icons/RemoveCircleOutline'; -import classNames from 'classnames'; -import get from 'lodash/get'; -import PropTypes from 'prop-types'; -import { Record, useTranslate, ValidationError } from 'ra-core'; import * as React from 'react'; import { Children, cloneElement, - FC, + MouseEvent, + MouseEventHandler, isValidElement, ReactElement, + ReactNode, useRef, } from 'react'; +import { Button, FormHelperText, Typography } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import AddIcon from '@material-ui/icons/AddCircleOutline'; +import CloseIcon from '@material-ui/icons/RemoveCircleOutline'; +import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward'; +import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward'; +import classNames from 'classnames'; +import get from 'lodash/get'; +import PropTypes from 'prop-types'; +import { Record, useTranslate, ValidationError } from 'ra-core'; import { FieldArrayRenderProps } from 'react-final-form-arrays'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; + import { ClassesOverride } from '../types'; +import { IconButtonWithTooltip } from '../button'; import FormInput from './FormInput'; -const useStyles = makeStyles( - theme => ({ - root: { - padding: 0, - marginBottom: 0, - '& > li:last-child': { - borderBottom: 'none', - }, - }, - line: { - display: 'flex', - listStyleType: 'none', - borderBottom: `solid 1px ${theme.palette.divider}`, - [theme.breakpoints.down('xs')]: { display: 'block' }, - '&.fade-enter': { - opacity: 0.01, - transform: 'translateX(100vw)', - }, - '&.fade-enter-active': { - opacity: 1, - transform: 'translateX(0)', - transition: 'all 500ms ease-in', - }, - '&.fade-exit': { - opacity: 1, - transform: 'translateX(0)', - }, - '&.fade-exit-active': { - opacity: 0.01, - transform: 'translateX(100vw)', - transition: 'all 500ms ease-in', - }, - }, - index: { - width: '3em', - paddingTop: '1em', - [theme.breakpoints.down('sm')]: { display: 'none' }, - }, - form: { flex: 2 }, - action: { - paddingTop: '0.5em', - }, - leftIcon: { - marginRight: theme.spacing(1), - }, - }), - { name: 'RaSimpleFormIterator' } -); - -const DefaultAddButton = props => { - const classes = useStyles(props); - const translate = useTranslate(); - return ( - - ); -}; - -const DefaultLabelFn = index => index + 1; - -const DefaultRemoveButton = props => { - const classes = useStyles(props); - const translate = useTranslate(); - return ( - - ); -}; - -const SimpleFormIterator: FC = props => { +export const SimpleFormIterator = (props: SimpleFormIteratorProps) => { const { addButton = , removeButton = , + reOrderButtons = , basePath, children, className, @@ -110,6 +42,7 @@ const SimpleFormIterator: FC = props => { disabled, disableAdd, disableRemove, + disableReordering, variant, margin, TransitionProps, @@ -137,16 +70,19 @@ const SimpleFormIterator: FC = props => { nextId.current > 0 ? Array.from(Array(nextId.current).keys()) : [] ); - const removeField = index => () => { + const removeField = (index: number) => () => { ids.current.splice(index, 1); - fields.remove(index); + fields?.remove(index); }; // Returns a boolean to indicate whether to disable the remove button for certain fields. // If disableRemove is a function, then call the function with the current record to // determining if the button should be disabled. Otherwise, use a boolean property that // enables or disables the button for all of the fields. - const disableRemoveField = (record, disableRemove) => { + const disableRemoveField = ( + record: Record, + disableRemove: boolean | DisableRemoveFunction + ) => { if (typeof disableRemove === 'boolean') { return disableRemove; } @@ -155,11 +91,13 @@ const SimpleFormIterator: FC = props => { const addField = () => { ids.current.push(nextId.current++); - fields.push(undefined); + fields?.push(undefined); }; // add field and call the onClick event of the button passed as addButton prop - const handleAddButtonClick = originalOnClickHandler => event => { + const handleAddButtonClick = ( + originalOnClickHandler: MouseEventHandler + ) => (event: MouseEvent) => { addField(); if (originalOnClickHandler) { originalOnClickHandler(event); @@ -168,15 +106,22 @@ const SimpleFormIterator: FC = props => { // remove field and call the onClick event of the button passed as removeButton prop const handleRemoveButtonClick = ( - originalOnClickHandler, - index - ) => event => { + originalOnClickHandler: MouseEventHandler, + index: number + ) => (event: MouseEvent) => { removeField(index)(); if (originalOnClickHandler) { originalOnClickHandler(event); } }; + const handleReorder = (origin: number, destination: number) => { + const item = ids.current[origin]; + ids.current[origin] = ids.current[destination]; + ids.current[destination] = item; + fields?.move(origin, destination); + }; + const records = get(record, source); return fields ? (
      @@ -195,12 +140,27 @@ const SimpleFormIterator: FC = props => { {...TransitionProps} >
    • - - {getItemLabel(index)} - +
      +
      + + {getItemLabel(index)} + + {!disabled && + !disableReordering && + cloneElement(reOrderButtons, { + index, + max: fields.length, + onReorder: handleReorder, + className: classNames( + 'button-reorder', + `button-reorder-${source}-${index}` + ), + })} +
      +
      {Children.map( children, @@ -322,12 +282,14 @@ export interface SimpleFormIteratorProps extends Partial, 'meta'>> { addButton?: ReactElement; basePath?: string; + children?: ReactNode; classes?: ClassesOverride; className?: string; defaultValue?: any; disabled?: boolean; disableAdd?: boolean; disableRemove?: boolean | DisableRemoveFunction; + disableReordering?: boolean; getItemLabel?: (index: number) => string; margin?: 'none' | 'normal' | 'dense'; meta?: { @@ -337,10 +299,118 @@ export interface SimpleFormIteratorProps }; record?: Record; removeButton?: ReactElement; + reOrderButtons?: ReactElement; resource?: string; source?: string; TransitionProps?: any; variant?: 'standard' | 'outlined' | 'filled'; } -export default SimpleFormIterator; +const useStyles = makeStyles( + theme => ({ + root: { + padding: 0, + marginBottom: 0, + '& > li:last-child': { + borderBottom: 'none', + }, + }, + line: { + display: 'flex', + listStyleType: 'none', + borderBottom: `solid 1px ${theme.palette.divider}`, + [theme.breakpoints.down('xs')]: { display: 'block' }, + '&.fade-enter': { + opacity: 0.01, + transform: 'translateX(100vw)', + }, + '&.fade-enter-active': { + opacity: 1, + transform: 'translateX(0)', + transition: 'all 500ms ease-in', + }, + '&.fade-exit': { + opacity: 1, + transform: 'translateX(0)', + }, + '&.fade-exit-active': { + opacity: 0.01, + transform: 'translateX(100vw)', + transition: 'all 500ms ease-in', + }, + }, + index: { + [theme.breakpoints.down('sm')]: { display: 'none' }, + marginRight: theme.spacing(1), + }, + indexContainer: { + display: 'flex', + paddingTop: '1em', + marginRight: theme.spacing(1), + alignItems: 'center', + }, + form: { flex: 2 }, + action: { + paddingTop: '0.5em', + }, + leftIcon: { + marginRight: theme.spacing(1), + }, + }), + { name: 'RaSimpleFormIterator' } +); + +const DefaultAddButton = props => { + const classes = useStyles(props); + const translate = useTranslate(); + return ( + + ); +}; + +const DefaultLabelFn = index => index + 1; + +const DefaultRemoveButton = props => { + const classes = useStyles(props); + const translate = useTranslate(); + return ( + + ); +}; + +const DefaultReOrderButtons = ({ + className, + index, + max, + onReorder, +}: { + className?: string; + index?: number; + max?: number; + onReorder?: (origin: number, destination: number) => void; +}) => ( +
      + onReorder(index, index - 1)} + disabled={index <= 0} + > + + + onReorder(index, index + 1)} + disabled={max == null || index >= max - 1} + > + + +
      +); diff --git a/packages/ra-ui-materialui/src/form/index.tsx b/packages/ra-ui-materialui/src/form/index.tsx index 1c414079041..e8cee28545a 100644 --- a/packages/ra-ui-materialui/src/form/index.tsx +++ b/packages/ra-ui-materialui/src/form/index.tsx @@ -1,14 +1,12 @@ import FormInput, { FormInputProps } from './FormInput'; import SimpleForm, { SimpleFormProps } from './SimpleForm'; -import SimpleFormIterator, { - SimpleFormIteratorProps, -} from './SimpleFormIterator'; import TabbedFormTabs, { TabbedFormTabsProps } from './TabbedFormTabs'; import Toolbar, { ToolbarProps } from './Toolbar'; import getFormInitialValues from './getFormInitialValues'; import { SimpleFormView, SimpleFormViewProps } from './SimpleFormView'; import { TabbedFormView, TabbedFormViewProps } from './TabbedFormView'; +export * from './SimpleFormIterator'; export * from './TabbedForm'; export * from './FormTab'; export * from './FormTabHeader'; @@ -16,7 +14,6 @@ export * from './FormTabHeader'; export type { FormInputProps, SimpleFormProps, - SimpleFormIteratorProps, TabbedFormTabsProps, SimpleFormViewProps, TabbedFormViewProps, @@ -27,7 +24,6 @@ export { FormInput, SimpleForm, SimpleFormView, - SimpleFormIterator, TabbedFormTabs, TabbedFormView, Toolbar, diff --git a/packages/ra-ui-materialui/src/input/ArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/ArrayInput.spec.tsx index 9258eb7ab5a..2949002322f 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput.spec.tsx @@ -6,7 +6,7 @@ import arrayMutators from 'final-form-arrays'; import ArrayInput from './ArrayInput'; import NumberInput from './NumberInput'; import TextInput from './TextInput'; -import SimpleFormIterator from '../form/SimpleFormIterator'; +import { SimpleFormIterator } from '../form/SimpleFormIterator'; import { minLength, required } from 'ra-core'; describe('', () => { diff --git a/packages/ra-ui-materialui/src/input/DateTimeInput.spec.tsx b/packages/ra-ui-materialui/src/input/DateTimeInput.spec.tsx index 424cdd65cbd..f7538bb2faf 100644 --- a/packages/ra-ui-materialui/src/input/DateTimeInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/DateTimeInput.spec.tsx @@ -8,7 +8,7 @@ import format from 'date-fns/format'; import DateTimeInput from './DateTimeInput'; import ArrayInput from './ArrayInput'; -import SimpleFormIterator from '../form/SimpleFormIterator'; +import { SimpleFormIterator } from '../form/SimpleFormIterator'; import { FormApi } from 'final-form'; describe('', () => { From ae466087ffddc621f7ffa6940b5bc10f9b7a1819 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 1 Sep 2021 16:18:23 +0200 Subject: [PATCH 2/3] Apply review --- .../src/form/SimpleFormIterator.spec.tsx | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/ra-ui-materialui/src/form/SimpleFormIterator.spec.tsx b/packages/ra-ui-materialui/src/form/SimpleFormIterator.spec.tsx index 415fe0d1db4..f21e5da66af 100644 --- a/packages/ra-ui-materialui/src/form/SimpleFormIterator.spec.tsx +++ b/packages/ra-ui-materialui/src/form/SimpleFormIterator.spec.tsx @@ -626,4 +626,50 @@ describe('', () => { fireEvent.click(getByText('Custom Remove Button')); expect(onClick).toHaveBeenCalled(); }); + + it('should display the custom add button', () => { + const { getByText } = renderWithRedux( + + + + + Custom Add Button} + > + + + + + + + ); + + expect(getByText('Custom Add Button')).not.toBeNull(); + }); + + it('should display the custom remove button', () => { + const { getByText } = renderWithRedux( + + + + + + Custom Remove Button + } + > + + + + + + + + ); + + expect(getByText('Custom Remove Button')).not.toBeNull(); + }); }); From 2601ea0ce423ad3a0c48d55489726ce0dfcabd04 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Wed, 1 Sep 2021 16:20:45 +0200 Subject: [PATCH 3/3] Revert Input documentation formatting changes --- docs/Inputs.md | 1119 +++++++++++++++++------------------------------- 1 file changed, 396 insertions(+), 723 deletions(-) diff --git a/docs/Inputs.md b/docs/Inputs.md index 15511bd757b..e992937b7c5 100644 --- a/docs/Inputs.md +++ b/docs/Inputs.md @@ -1,6 +1,6 @@ --- layout: default -title: 'Input Components' +title: "Input Components" --- # Input Components @@ -9,33 +9,17 @@ An `Input` component displays an input, or a dropdown list, a list of radio butt ```jsx // in src/posts.js -import * as React from 'react'; -import { - Edit, - SimpleForm, - ReferenceInput, - SelectInput, - TextInput, - required, -} from 'react-admin'; +import * as React from "react"; +import { Edit, SimpleForm, ReferenceInput, SelectInput, TextInput, required } from 'react-admin'; -export const PostEdit = props => ( +export const PostEdit = (props) => ( } {...props}> - + - + @@ -46,15 +30,15 @@ export const PostEdit = props => ( All input components accept the following props: -| Prop | Required | Type | Default | Description | -| --------------- | -------- | ------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `source` | Required | `string` | - | Name of the entity property to use for the input value | +| Prop | Required | Type | Default | Description | +| --------------- | -------- | ------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `source` | Required | `string` | - | Name of the entity property to use for the input value | | `label` | Optional | `string` | - | Input label. In i18n apps, the label is passed to the `translate` function. Defaults to the humanized `source` when omitted. Set `label={false}` to hide the label. | -| `validate` | Optional | `Function` | `array` | - | Validation rules for the current property. See the [Validation Documentation](./CreateEdit.md#validation) for details. | -| `helperText` | Optional | `string` | - | Text to be displayed under the input | -| `fullWidth` | Optional | `boolean` | `false` | If `true`, the input will expand to fill the form width | -| `className` | Optional | `string` | - | Class name (usually generated by JSS) to customize the look and feel of the field element itself | -| `formClassName` | Optional | `string` | - | Class name to be applied to the container of the input (e.g. the `
      ` forming each row in ``) | +| `validate` | Optional | `Function` | `array` | - | Validation rules for the current property. See the [Validation Documentation](./CreateEdit.md#validation) for details. | +| `helperText` | Optional | `string` | - | Text to be displayed under the input | +| `fullWidth` | Optional | `boolean` | `false` | If `true`, the input will expand to fill the form width | +| `className` | Optional | `string` | - | Class name (usually generated by JSS) to customize the look and feel of the field element itself | +| `formClassName` | Optional | `string` | - | Class name to be applied to the container of the input (e.g. the `
      ` forming each row in ``) | ```jsx @@ -102,7 +86,7 @@ Then you can display a text input to edit the author first name as follows: ```jsx import { BooleanInput } from 'react-admin'; -; + ``` ![BooleanInput](./img/boolean-input.png) @@ -112,7 +96,6 @@ This input does not handle `null` values. You would need the `, }} -/>; +/> ``` - {% endraw %} ![CustomBooleanInputCheckIcon](./img/custom-switch-icon.png) @@ -136,7 +118,7 @@ Refer to [Material UI Switch documentation](https://material-ui.com/api/switch) ```jsx import { NullableBooleanInput } from 'react-admin'; -; + ``` ![NullableBooleanInput](./img/nullable-boolean-input.gif) @@ -152,7 +134,7 @@ englishMessages.ra.boolean.false = 'False label'; englishMessages.ra.boolean.true = 'True label'; const i18nProvider = polyglotI18nProvider(() => englishMessages, 'en'); -; + ``` Additionally, individual instances of `NullableBooleanInput` may be customized by setting the `nullLabel`, `falseLabel` and `trueLabel` properties. Values specified for those properties will be translated by react-admin. @@ -166,7 +148,7 @@ import { NullableBooleanInput } from 'react-admin'; nullLabel="Either" falseLabel="No" trueLabel="Yes" -/>; +/> ``` ![NullableBooleanInput](./img/nullable-boolean-input-null-label.png) @@ -192,7 +174,7 @@ The appearance of `` depends on the browser, and falls back to a text ```jsx import { DateInput } from 'react-admin'; -; + ``` `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -210,7 +192,7 @@ An input for editing dates with time. `` renders a standard brows ```jsx import { DateTimeInput } from 'react-admin'; -; + ``` `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -225,17 +207,17 @@ import { DateTimeInput } from 'react-admin'; #### Properties -| Prop | Required | Type | Default | Description | -| --------------- | -------- | --------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `accept` | Optional | `string | string[]` | - | Accepted file type(s), e. g. 'image/\*,.pdf'. If left empty, all file types are accepted. Equivalent of the `accept` attribute of an ``. See [MDN input docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept) for syntax and examples. | -| `children` | Optional | `ReactNode` | - | Element used to display the preview of an image (cloned several times if the select accepts multiple files). | -| `minSize` | Optional | `number` | 0 | Minimum image size (in bytes), e.g. 5000 for 5KB | -| `maxSize` | Optional | `number` | `Infinity` | Maximum image size (in bytes), e.g. 5000000 for 5MB | -| `multiple` | Optional | `boolean` | `false` | Set to true if the input should accept a list of images, false if it should only accept one image | -| `labelSingle` | Optional | `string` | 'ra.input.image. upload_single' | Invite displayed in the drop zone if the input accepts one image | -| `labelMultiple` | Optional | `string` | 'ra.input.file. upload_multiple' | Invite displayed in the drop zone if the input accepts several images | -| `placeholder` | Optional | `string` | `ReactNode` | - | Invite displayed in the drop zone, overrides `labelSingle` and `labelMultiple` | -| `options` | Optional | `Object` | `{}` | Additional options passed to react-dropzone's `useDropzone()` hook. See [the react-dropzone source](https://github.com/react-dropzone/react-dropzone/blob/master/src/index.js) for details . | +| Prop | Required | Type | Default | Description | +| --------------- | -------- | --------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `accept` | Optional | `string | string[]` | - | Accepted file type(s), e. g. 'image/*,.pdf'. If left empty, all file types are accepted. Equivalent of the `accept` attribute of an ``. See [MDN input docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept) for syntax and examples. | +| `children` | Optional | `ReactNode` | - | Element used to display the preview of an image (cloned several times if the select accepts multiple files). | +| `minSize` | Optional | `number` | 0 | Minimum image size (in bytes), e.g. 5000 for 5KB | +| `maxSize` | Optional | `number` | `Infinity` | Maximum image size (in bytes), e.g. 5000000 for 5MB | +| `multiple` | Optional | `boolean` | `false` | Set to true if the input should accept a list of images, false if it should only accept one image | +| `labelSingle` | Optional | `string` | 'ra.input.image. upload_single' | Invite displayed in the drop zone if the input accepts one image | +| `labelMultiple` | Optional | `string` | 'ra.input.file. upload_multiple' | Invite displayed in the drop zone if the input accepts several images | +| `placeholder` | Optional | `string` | `ReactNode` | - | Invite displayed in the drop zone, overrides `labelSingle` and `labelMultiple` | +| `options` | Optional | `Object` | `{}` | Additional options passed to react-dropzone's `useDropzone()` hook. See [the react-dropzone source](https://github.com/react-dropzone/react-dropzone/blob/master/src/index.js) for details . | `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -260,12 +242,7 @@ The `ImageInput` component accepts an `options` prop, allowing to set the [react If the default Dropzone label doesn't fit with your need, you can pass a `placeholder` prop to overwrite it. The value can be anything React can render (`PropTypes.node`): ```jsx -Drop your file here

      } -> +Drop your file here

      }>
      ``` @@ -321,17 +298,12 @@ Writing a custom preview component is quite straightforward: it's a standard [fi When receiving **new** files, `FileInput` will add a `rawFile` property to the object passed as the `record` prop of children. This `rawFile` is the [File](https://developer.mozilla.org/en-US/docs/Web/API/File) instance of the newly added file. This can be useful to display information about size or MIME type inside a custom field. -The `FileInput` component accepts an `options` prop into which you can pass all the [react-dropzone properties](https://react-dropzone.netlify.com/#proptypes). +The `FileInput` component accepts an `options` prop into which you can pass all the [react-dropzone properties](https://react-dropzone.netlify.com/#proptypes). If the default Dropzone label doesn't fit with your need, you can pass a `placeholder` prop to overwrite it. The value can be anything React can render (`PropTypes.node`): ```jsx -Drop your file here

      } -> +Drop your file here

      }>
      ``` @@ -382,7 +354,7 @@ It is necessary for numeric values because of a [known React bug](https://github ```jsx import { NumberInput } from 'react-admin'; -; + ``` #### Properties @@ -409,7 +381,7 @@ You can customize the `step` props (which defaults to "any"). For instance, to r ```jsx import { PasswordInput } from 'react-admin'; -; + ``` ![Password Input](./img/password-input.png) @@ -418,7 +390,7 @@ It is possible to change the default behavior and display the value by default v ```jsx import { PasswordInput } from 'react-admin'; -; + ``` ![Password Input (visible)](./img/password-input-visible.png) @@ -426,14 +398,9 @@ import { PasswordInput } from 'react-admin'; **Tip**: It is possible to set the [`autocomplete` attribute](https://developer.mozilla.org/fr/docs/Web/HTML/Attributs/autocomplete) by injecting an input props: {% raw %} - ```jsx - + ``` - {% endraw %} ### `` @@ -454,57 +421,50 @@ Then use it as a normal input component: ```jsx import RichTextInput from 'ra-input-rich-text'; -; + ``` You can customize the rich text editor toolbar using the `toolbar` attribute, as described on the [Quill official toolbar documentation](https://quilljs.com/docs/modules/toolbar/). ```jsx - + ``` If you need to add Quill `modules` or `themes`, you can do so by passing them in the `options` prop. {% raw %} - ```jsx ``` - {% endraw %} If you need more customization, you can access the quill object through the `configureQuill` callback that will be called just after its initialization. ```jsx -const configureQuill = quill => - quill.getModule('toolbar').addHandler('bold', function (value) { - this.quill.format('bold', value); - }); +const configureQuill = quill => quill.getModule('toolbar').addHandler('bold', function (value) { + this.quill.format('bold', value) +}); // ... -; + ``` `` also accepts the [common input props](./Inputs.md#common-input-props). -**Tip**: When used inside a material-ui `` (e.g in the default `` view), `` displays link tooltip as cut off when the user wants to add a hyperlink to a word located on the left side of the input. This is due to an incompatibility between material-ui's `` component and Quill's positioning algorithm for the link tooltip. +**Tip**: When used inside a material-ui `` (e.g in the default `` view), `` displays link tooltip as cut off when the user wants to add a hyperlink to a word located on the left side of the input. This is due to an incompatibility between material-ui's `` component and Quill's positioning algorithm for the link tooltip. To fix this problem, you should override the default card style, as follows: @@ -532,7 +492,7 @@ import { Edit, SimpleForm, TextInput } from 'react-admin'; ```jsx import { TextInput } from 'react-admin'; -; + ``` #### Properties @@ -564,7 +524,7 @@ You can make the `` component resettable using the `resettable` prop. ```jsx import { TextInput } from 'react-admin'; -; + ``` ![resettable TextInput](./img/resettable-text-input.gif) @@ -585,37 +545,34 @@ Set the `choices` attribute to determine the options list (with `id`, `name` tup ```jsx import { AutocompleteInput } from 'react-admin'; -; + ``` #### Properties -| Prop | Required | Type | Default | Description | -| ------------------------- | -------- | --------------------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `allowEmpty` | Optional | `boolean` | `false` | If `false` and the `searchText` typed did not match any suggestion, the `searchText` will revert to the current value when the field is blurred. If `true` and the `searchText` is set to `''` then the field will set the input value to `null`. | -| `clearAlwaysVisible` | Optional | `boolean` | `false` | When `resettable` is true, set this prop to `true` to have the Reset button visible even when the field is empty | -| `choices` | Required | `Object[]` | `-` | List of items to autosuggest | -| `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 | -| `emptyValue` | Optional | `any` | `''` | The value to use for the empty element | -| `emptyText` | Optional | `string` | `''` | The text to use for the empty element | -| `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 | -| `inputText` | Optional | `Function` | `-` | If `optionText` is a custom Component, this function is needed to determine the text displayed for the current selection. | -| `resettable` | Optional | `boolean` | `false` | Display a button to reset the text filter. Useful when using `` inside the list filters | -| `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 react-autosuggest component. Ex.`(value) => value.trim() > 2` | -| `suggestionLimit` | Optional | `number` | `null` | Limits the numbers of suggestions that are shown in the dropdown list | +| Prop | Required | Type | Default | Description | +| ------------------------- | -------- | -------------- | ------------ | ------------------------------------ | +| `allowEmpty` | Optional | `boolean` | `false` | If `false` and the `searchText` typed did not match any suggestion, the `searchText` will revert to the current value when the field is blurred. If `true` and the `searchText` is set to `''` then the field will set the input value to `null`. | +| `clearAlwaysVisible` | Optional | `boolean` | `false` | When `resettable` is true, set this prop to `true` to have the Reset button visible even when the field is empty | +| `choices` | Required | `Object[]` | `-` | List of items to autosuggest | +| `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 | +| `emptyValue` | Optional | `any` | `''` | The value to use for the empty element | +| `emptyText` | Optional | `string` | `''` | The text to use for the empty element | +| `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 | +| `inputText` | Optional | `Function` | `-` | If `optionText` is a custom Component, this function is needed to determine the text displayed for the current selection. | +| `resettable` | Optional | `boolean` | `false` | Display a button to reset the text filter. Useful when using `` inside the list filters | +| `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 react-autosuggest component. Ex.`(value) => value.trim() > 2` | +| `suggestionLimit` | Optional | `number` | `null` | Limits the numbers of suggestions that are shown in the dropdown list | `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -628,35 +585,26 @@ const choices = [ { _id: 123, full_name: 'Leo Tolstoi', sex: 'M' }, { _id: 456, full_name: 'Jane Austen', sex: 'F' }, ]; -; + ``` `optionText` also accepts a function, so you can shape the option text at will: ```jsx const choices = [ - { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, - { id: 456, first_name: 'Jane', last_name: 'Austen' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`; -; + ``` `optionText` also accepts a custom Component. However, as the underlying Autocomplete component requires that the current selection is a string, if you opt for a Component, you must pass a function as the `inputText` prop. This function should return text representation of the current selection: ```jsx const choices = [ - { id: 123, first_name: 'Leo', last_name: 'Tolstoi', avatar: '/pengouin' }, - { id: 456, first_name: 'Jane', last_name: 'Austen', avatar: '/panda' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi', avatar:'/pengouin' }, + { id: 456, first_name: 'Jane', last_name: 'Austen', avatar:'/panda' }, ]; const OptionRenderer = choice => ( @@ -670,15 +618,15 @@ const inputText = choice => `${choice.first_name} ${choice.last_name}`; choices={choices} optionText={} inputText={inputText} -/>; +/> ``` 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: 'M', name: 'myroot.gender.male' }, + { id: 'F', name: 'myroot.gender.female' }, ]; ``` @@ -686,7 +634,7 @@ However, in some cases (e.g. inside a ``), you may not want the In that case, set the `translateChoice` prop to `false`. ```jsx - + ``` If you want to limit the initial choices shown to the current value only, you can set the `limitChoicesToValue` prop. @@ -697,16 +645,11 @@ Ex. ` { return val.trim().le `` renders a [material-ui `` component](https://material-ui.com/api/text-field/). Use the `options` attribute to override any of the `` attributes: {% raw %} - ```jsx - + ``` - {% endraw %} **Tip**: If you want to populate the `choices` attribute with a list of related records, you should decorate `` with [``](#referenceinput), and leave the `choices` empty: @@ -716,27 +659,21 @@ import { AutocompleteInput, ReferenceInput } from 'react-admin'; -; + ``` Lastly, would you need to override the props of the suggestion's container (a `Popper` element), you can specify them using the `options.suggestionsContainerProps`. For example: {% raw %} - ```jsx - + ``` - {% endraw %} -**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 [``](#referenceinput) doesn't cover your need), you'll have to [write your own Input component](#writing-your-own-input-component) based on material-ui `` component. +**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 [``](#referenceinput) doesn't cover your need), you'll have to [write your own Input component](#writing-your-own-input-component) based on material-ui `` component. #### Creating New Choices @@ -745,11 +682,10 @@ The `` can allow users to create a new choice if either the ` Use the `onCreate` prop when you only require users to provide a simple string and a `prompt` is enough. You can return either the new choice directly or a Promise resolving to the new choice. {% raw %} - ```js import { AutocompleteInput, Create, SimpleForm, TextInput } from 'react-admin'; -const PostCreate = props => { +const PostCreate = (props) => { const categories = [ { name: 'Tech', id: 'tech' }, { name: 'Lifestyle', id: 'lifestyle' }, @@ -761,10 +697,7 @@ const PostCreate = props => { { const newCategoryName = prompt('Enter a new category'); - const newCategory = { - id: newCategoryName.toLowerCase(), - name: newCategoryName, - }; + const newCategory = { id: newCategoryName.toLowerCase(), name: newCategoryName }; categories.push(newCategory); return newCategory; }} @@ -774,15 +707,13 @@ const PostCreate = props => {
      ); -}; +} ``` - {% endraw %} Use the `create` prop when you want a more polished or complex UI. For example a Material UI `` asking for multiple fields because the choices are from a referenced resource. {% raw %} - ```js import { AutocompleteInput, @@ -790,7 +721,7 @@ import { ReferenceInput, SimpleForm, TextInput, - useCreateSuggestionContext, + useCreateSuggestionContext } from 'react-admin'; import { @@ -803,7 +734,7 @@ import { TextField, } from '@material-ui/core'; -const PostCreate = props => { +const PostCreate = (props) => { return ( @@ -814,14 +745,14 @@ const PostCreate = props => { ); -}; +} const CreateCategory = () => { const { filter, onCancel, onCreate } = useCreateSuggestionContext(); const [value, setValue] = React.useState(filter || ''); const [create] = useCreate('categories'); - const handleSubmit = event => { + const handleSubmit = (event) => { event.preventDefault(); create( { @@ -860,7 +791,6 @@ const CreateCategory = () => { ); }; ``` - {% endraw %} #### CSS API @@ -884,14 +814,11 @@ Set the `choices` attribute to determine the options (with `id`, `name` tuples): ```jsx import { RadioButtonGroupInput } from 'react-admin'; -; + ``` #### Properties @@ -916,80 +843,54 @@ const choices = [ { _id: 123, full_name: 'Leo Tolstoi', sex: 'M' }, { _id: 456, full_name: 'Jane Austen', sex: 'F' }, ]; -; + ``` `optionText` also accepts a function, so you can shape the option text at will: ```jsx const choices = [ - { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, - { id: 456, first_name: 'Jane', last_name: 'Austen' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { 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 cloned and receive 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' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; -const FullNameField = ({ record }) => ( - - {record.first_name} {record.last_name} - -); -} -/>; +const FullNameField = ({ record }) => {record.first_name} {record.last_name}; +}/> ``` 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: 'M', name: 'myroot.gender.male' }, + { id: 'F', name: 'myroot.gender.female' }, ]; ``` 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 - + ``` Lastly, use the `options` attribute if you want to override any of Material UI's `` attributes: {% raw %} - ```jsx - + ``` - {% endraw %} Refer to [Material UI RadioGroup documentation](https://material-ui.com/api/radio-group) for more details. @@ -1001,7 +902,7 @@ import { RadioButtonGroupInput, ReferenceInput } from 'react-admin'; -; + ``` #### CSS API @@ -1024,31 +925,28 @@ Set the `choices` attribute to determine the options (with `id`, `name` tuples): ```jsx import { SelectInput } from 'react-admin'; -; + ``` #### Properties -| Prop | Required | Type | Default | Description | -| ----------------- | -------- | -------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | -| `allowEmpty` | Optional | `boolean` | `false` | If true, the first option is an empty one | -| `choices` | Required | `Object[]` | - | List of items to show as options | -| `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 | -| `emptyText` | Optional | `string` | '' | The text to display for the empty option | -| `onCreate` | Optional | `Function` | `-` | A function called with the current filter value when users choose to create a new choice. | -| `options` | Optional | `Object` | - | Props to pass to the underlying `` element | -| `optionText` | Optional | `string` | `Function` | `name` | Field name of record to display in the suggestion item or function which accepts the current record as argument (`record => {string}`) | -| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | -| `resettable` | Optional | `boolean` | `false` | If `true`, display a button to reset the changes in this input value | -| `translateChoice` | Optional | `boolean` | `true` | Whether the choices should be translated | +| Prop | Required | Type | Default | Description | +| ----------------- | -------- | -------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `allowEmpty` | Optional | `boolean` | `false` | If true, the first option is an empty one | +| `choices` | Required | `Object[]` | - | List of items to show as options | +| `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 | +| `emptyText` | Optional | `string` | '' | The text to display for the empty option | +| `onCreate` | Optional | `Function` | `-` | A function called with the current filter value when users choose to create a new choice. | +| `options` | Optional | `Object` | - | Props to pass to the underlying `` element | +| `optionText` | Optional | `string` | `Function` | `name` | Field name of record to display in the suggestion item or function which accepts the current record as argument (`record => {string}`) | +| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | +| `resettable` | Optional | `boolean` | `false` | If `true`, display a button to reset the changes in this input value | +| `translateChoice` | Optional | `boolean` | `true` | Whether the choices should be translated | `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -1061,76 +959,54 @@ const choices = [ { _id: 123, full_name: 'Leo Tolstoi', sex: 'M' }, { _id: 456, full_name: 'Jane Austen', sex: 'F' }, ]; -; + ``` `optionText` also accepts a function, so you can shape the option text at will: ```jsx const choices = [ - { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, - { id: 456, first_name: 'Jane', last_name: 'Austen' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { 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 cloned and receive 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' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; -const FullNameField = ({ record }) => ( - - {record.first_name} {record.last_name} - -); -} -/>; +const FullNameField = ({ record }) => {record.first_name} {record.last_name}; +}/> ``` Enabling the `allowEmpty` props adds an empty choice (with a default `''` value, which you can overwrite with the `emptyValue` prop) on top of the options. You can furthermore customize the `MenuItem` for the empty choice by using the `emptyText` prop, which can receive either a string or a React Element, which doesn't receive any props. ```jsx - + ``` 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: 'M', name: 'myroot.gender.male' }, + { id: 'F', name: 'myroot.gender.female' }, ]; ``` However, in some cases, you may not want the choice to be translated. In that case, set the `translateChoice` prop to `false`. ```jsx - + ``` Note that `translateChoice` is set to `false` when `` is a child of ``. @@ -1138,16 +1014,11 @@ Note that `translateChoice` is set to `false` when `` is a child of Lastly, use the `options` attribute if you want to override any of Material UI's `` attributes: {% raw %} - ```jsx - + ``` - {% endraw %} Refer to [Material UI Select documentation](https://material-ui.com/api/select) for more details. @@ -1159,7 +1030,7 @@ import { SelectInput, ReferenceInput } from 'react-admin'; -; + ``` If, instead of showing choices as a dropdown list, you prefer to display them as a list of radio buttons, try the [``](#radiobuttongroupinput). And if the list is too big, prefer the [``](#autocompleteinput). @@ -1176,12 +1047,7 @@ const choices = [ { _id: 456, full_name: 'Jane Austen', sex: 'F' }, { _id: 1, full_name: 'System Administrator', sex: 'F', disabled: true }, ]; -; + ``` You can use a custom field name by setting `disableValue` prop: @@ -1192,13 +1058,7 @@ const choices = [ { _id: 456, full_name: 'Jane Austen', sex: 'F' }, { _id: 987, full_name: 'Jack Harden', sex: 'M', not_available: true }, ]; -; + ``` #### Creating New Choices @@ -1208,11 +1068,10 @@ The `` can allow users to create a new choice if either the `create Use the `onCreate` prop when you only require users to provide a simple string and a `prompt` is enough. You can return either the new choice directly or a Promise resolving to the new choice. {% raw %} - ```js import { SelectInput, Create, SimpleForm, TextInput } from 'react-admin'; -const PostCreate = props => { +const PostCreate = (props) => { const categories = [ { name: 'Tech', id: 'tech' }, { name: 'Lifestyle', id: 'lifestyle' }, @@ -1224,10 +1083,7 @@ const PostCreate = props => { { const newCategoryName = prompt('Enter a new category'); - const newCategory = { - id: newCategoryName.toLowerCase(), - name: newCategoryName, - }; + const newCategory = { id: newCategoryName.toLowerCase(), name: newCategoryName }; categories.push(newCategory); return newCategory; }} @@ -1237,15 +1093,13 @@ const PostCreate = props => { ); -}; +} ``` - {% endraw %} Use the `create` prop when you want a more polished or complex UI. For example a Material UI `` asking for multiple fields because the choices are from a referenced resource. {% raw %} - ```js import { SelectInput, @@ -1253,7 +1107,7 @@ import { ReferenceInput, SimpleForm, TextInput, - useCreateSuggestionContext, + useCreateSuggestionContext } from 'react-admin'; import { @@ -1266,7 +1120,7 @@ import { TextField, } from '@material-ui/core'; -const PostCreate = props => { +const PostCreate = (props) => { return ( @@ -1277,14 +1131,14 @@ const PostCreate = props => { ); -}; +} const CreateCategory = () => { const { filter, onCancel, onCreate } = useCreateSuggestionContext(); const [value, setValue] = React.useState(filter || ''); const [create] = useCreate('categories'); - const handleSubmit = event => { + const handleSubmit = (event) => { event.preventDefault(); create( { @@ -1323,7 +1177,6 @@ const CreateCategory = () => { ); }; ``` - {% endraw %} #### CSS API @@ -1343,19 +1196,14 @@ To edit arrays of data embedded inside a record, `` creates a list o ![ArrayInput](./img/array-input.gif) ```jsx -import { - ArrayInput, - SimpleFormIterator, - DateInput, - TextInput, -} from 'react-admin'; +import { ArrayInput, SimpleFormIterator, DateInput, TextInput } from 'react-admin'; -; + ``` `` allows editing of embedded arrays, like the `authors` field in the following `post` record: @@ -1372,7 +1220,7 @@ import { "user_id": 456, "url": "co_writer", } - ] + ] } ``` @@ -1380,77 +1228,52 @@ import { `` expects a single child, which must be a *form iterator* component. A form iterator is a component accepting a `fields` object as passed by [react-final-form-array](https://github.com/final-form/react-final-form-arrays#fieldarrayrenderprops), and defining a layout for an array of fields. For instance, the `` component 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 (a backlink in this example). -You can pass `disableAdd`, `disableRemove` and `disableReordering` as props of `SimpleFormIterator`, to disable `ADD`, `REMOVE` and reordering buttons respectively. They are all enabled by default. +You can pass `disableAdd` and `disableRemove` as props of `SimpleFormIterator`, to disable `ADD` and `REMOVE` button respectively. Default value of both is `false`. ```jsx -import { - ArrayInput, - SimpleFormIterator, - DateInput, - TextInput, -} from 'react-admin'; +import { ArrayInput, SimpleFormIterator, DateInput, TextInput } from 'react-admin'; - + -; + ``` -You can also use `addButton`, `removeButton` or `reOrderButtons` props to pass your custom add and remove buttons to `SimpleFormIterator`. +You can also use `addButton` and `removeButton` props to pass your custom add and remove buttons to `SimpleFormIterator`. ```jsx -import { - ArrayInput, - SimpleFormIterator, - DateInput, - TextInput, -} from 'react-admin'; +import { ArrayInput, SimpleFormIterator, DateInput, TextInput } from 'react-admin'; - } - removeButton={} - reOrderButtons={} - > + } removeButton={}> -; + ``` Furthermore, if you want to customize the label displayed for each item, you can pass a function to `` via the `getItemLabel` prop. ```jsx -import { - ArrayInput, - SimpleFormIterator, - DateInput, - TextInput, -} from 'react-admin'; +import { ArrayInput, SimpleFormIterator, DateInput, TextInput } from 'react-admin'; - `${index + 1}. link`}> + `${index + 1}. link`}> -; + ``` **Note**: `` only accepts `Input` components as children. If you want to use some `Fields` instead, you have to use a `` to get the correct source, as follows: ```jsx -import { - ArrayInput, - SimpleFormIterator, - DateInput, - TextInput, - FormDataConsumer, -} from 'react-admin'; +import { ArrayInput, SimpleFormIterator, DateInput, TextInput, FormDataConsumer } from 'react-admin'; - + {({ getSource, scopedFormData }) => { @@ -1463,10 +1286,10 @@ import { }} -; + ``` -`` also accepts the [common input props](./Inputs.md#common-input-props) (except `format` and `parse`). +`` also accepts the [common input props](./Inputs.md#common-input-props) (except `format` and `parse`). **Important**: Note that asynchronous validators are not supported on the `` component due to a limitation of [react-final-form-arrays](https://github.com/final-form/react-final-form-arrays). @@ -1482,35 +1305,32 @@ Set the `choices` attribute to determine the options list (with `id`, `name` tup ```jsx import { AutocompleteArrayInput } from 'react-admin'; -; + ``` #### Properties -| Prop | Required | Type | Default | Description | -| ------------------------- | -------- | -------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `allowEmpty` | Optional | `boolean` | `false` | If `true`, the first option is an empty one | -| `allowDuplicates` | Optional | `boolean` | `false` | If `true`, the options can be selected several times | -| `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. | -| `choices` | Required | `Object[]` | - | List of items to autosuggest | -| `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. | -| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | -| `optionText` | Optional | `string` | `Function` | `name` | Field name of record to display in the suggestion item or function which accepts the current record as argument (`record => {string}`) | -| `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 react-autosuggest component. Ex.`(value) => value.trim() > 2` | -| `source` | Required | `string` | - | Name of field to edit, its type should match the type retrieved from `optionValue` | -| `suggestionLimit` | Optional | `number` | `null` | Limits the numbers of suggestions that are shown in the dropdown list | +| Prop | Required | Type | Default | Description | +| ------------------------- | -------- | -------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `allowEmpty` | Optional | `boolean` | `false` | If `true`, the first option is an empty one | +| `allowDuplicates` | Optional | `boolean` | `false` | If `true`, the options can be selected several times | +| `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. | +| `choices` | Required | `Object[]` | - | List of items to autosuggest | +| `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. | +| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | +| `optionText` | Optional | `string` | `Function` | `name` | Field name of record to display in the suggestion item or function which accepts the current record as argument (`record => {string}`) | +| `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 react-autosuggest component. Ex.`(value) => value.trim() > 2` | +| `source` | Required | `string` | - | Name of field to edit, its type should match the type retrieved from `optionValue` | +| `suggestionLimit` | Optional | `number` | `null` | Limits the numbers of suggestions that are shown in the dropdown list | `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -1523,46 +1343,33 @@ const choices = [ { _id: 123, full_name: 'Leo Tolstoi', sex: 'M' }, { _id: 456, full_name: 'Jane Austen', sex: 'F' }, ]; -; + ``` `optionText` also accepts a function, so you can shape the option text at will: ```jsx const choices = [ - { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, - { id: 456, first_name: 'Jane', last_name: 'Austen' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`; -; + ``` 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: 'M', name: 'myroot.gender.male' }, + { id: 'F', name: 'myroot.gender.female' }, ]; ``` 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). @@ -1571,16 +1378,11 @@ Ex. ` { return val.trim Lastly, `` renders a [material-ui `` component](https://material-ui.com/api/text-field/). Use the `options` attribute to override any of the `` attributes: {% raw %} - ```jsx - + ``` - {% endraw %} **Tip**: Like many other inputs, `` accept a `fullWidth` prop. @@ -1592,27 +1394,21 @@ import { AutocompleteArrayInput, ReferenceArrayInput } from 'react-admin'; -; + ``` If you need to override the props of the suggestion's container (a `Popper` element), you can specify them using the `options.suggestionsContainerProps`. For example: {% raw %} - ```jsx - + ``` - {% endraw %} -**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) doesn't cover your need), you'll have to [write your own Input component](#writing-your-own-input-component) based on [material-ui-chip-input](https://github.com/TeamWertarbyte/material-ui-chip-input). +**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) doesn't cover your need), you'll have to [write your own Input component](#writing-your-own-input-component) based on [material-ui-chip-input](https://github.com/TeamWertarbyte/material-ui-chip-input). **Tip**: React-admin's `` has only a capital A, while material-ui's `` has a capital A and a capital C. Don't mix up the components! @@ -1623,16 +1419,10 @@ The `` can allow users to create a new choice if either Use the `onCreate` prop when you only require users to provide a simple string and a `prompt` is enough. 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'; +import { AutocompleteArrayInput, Create, SimpleForm, TextInput } from 'react-admin'; -const PostCreate = props => { +const PostCreate = (props) => { const tags = [ { name: 'Tech', id: 'tech' }, { name: 'Lifestyle', id: 'lifestyle' }, @@ -1644,10 +1434,7 @@ const PostCreate = props => { { const newTagName = prompt('Enter a new tag'); - const newTag = { - id: newTagName.toLowerCase(), - name: newTagName, - }; + const newTag = { id: newTagName.toLowerCase(), name: newTagName }; categories.push(newTag); return newTag; }} @@ -1657,15 +1444,13 @@ const PostCreate = props => { ); -}; +} ``` - {% endraw %} Use the `create` prop when you want a more polished or complex UI. For example a Material UI `` asking for multiple fields because the choices are from a referenced resource. {% raw %} - ```js import { AutocompleteArrayInput, @@ -1673,7 +1458,7 @@ import { ReferenceArrayInput, SimpleForm, TextInput, - useCreateSuggestionContext, + useCreateSuggestionContext } from 'react-admin'; import { @@ -1686,7 +1471,7 @@ import { TextField, } from '@material-ui/core'; -const PostCreate = props => { +const PostCreate = (props) => { return ( @@ -1697,14 +1482,14 @@ const PostCreate = props => { ); -}; +} const CreateTag = () => { const { filter, onCancel, onCreate } = useCreateSuggestionContext(); const [value, setValue] = React.useState(filter || ''); const [create] = useCreate('tags'); - const handleSubmit = event => { + const handleSubmit = (event) => { event.preventDefault(); create( { @@ -1743,7 +1528,6 @@ const CreateTag = () => { ); }; ``` - {% endraw %} #### CSS API @@ -1772,20 +1556,17 @@ Set the `choices` attribute to determine the options (with `id`, `name` tuples): ```jsx import { CheckboxGroupInput } from 'react-admin'; -; + ``` #### Properties | Prop | Required | Type | Default | Description | -| ------------- | -------- | -------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| ------------- | -------- | -------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `choices` | Required | `Object[]` | - | List of choices | | `optionText` | Optional | `string` | `Function` | `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 | @@ -1804,46 +1585,29 @@ const choices = [ { _id: 123, full_name: 'Leo Tolstoi', sex: 'M' }, { _id: 456, full_name: 'Jane Austen', sex: 'F' }, ]; -; + ``` `optionText` also accepts a function, so you can shape the option text at will: ```jsx const choices = [ - { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, - { id: 456, first_name: 'Jane', last_name: 'Austen' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { 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 cloned and receive 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' }, + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, ]; -const FullNameField = ({ record }) => ( - - {record.first_name} {record.last_name} - -); -} -/>; +const FullNameField = ({ record }) => {record.first_name} {record.last_name}; +}/> ``` The choices are translated by default, so you can use translation identifiers as choices: @@ -1859,25 +1623,20 @@ const choices = [ 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 - + ``` Lastly, use the `options` attribute if you want to override any of Material UI's `` attributes: {% raw %} - ```jsx import { FavoriteBorder, Favorite } from '@material-ui/icons'; -, - checkedIcon: , - }} -/>; +, + checkedIcon: +}} /> ``` - {% endraw %} #### CSS API @@ -1901,7 +1660,7 @@ import { DualListInput } from '@react-admin/ra-relationships'; -; + ``` Check [the `ra-relationships` documentation](https://marmelab.com/ra-enterprise/modules/ra-relationships) for more details. @@ -1914,19 +1673,19 @@ To let users choose several values in a list using a dropdown, use `` element | -| `optionText` | Optional | `string` | `Function` | `name` | Field name of record to display in the suggestion item or function which accepts the current record as argument (`record => {string}`) | -| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | -| `resettable` | Optional | `boolean` | `false` | If `true`, display a button to reset the changes in this input value | -| `translateChoice` | Optional | `boolean` | `true` | Whether the choices should be translated | +| Prop | Required | Type | Default | Description | +| ----------------- | -------- | -------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `allowEmpty` | Optional | `boolean` | `false` | If true, the first option is an empty one | +| `choices` | Required | `Object[]` | - | List of items to show as options | +| `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 | +| `emptyText` | Optional | `string` | '' | The text to display for the empty option | +| `onCreate` | Optional | `Function` | `-` | A function called with the current filter value when users choose to create a new choice. | +| `options` | Optional | `Object` | - | Props to pass to the underlying `` element | +| `optionText` | Optional | `string` | `Function` | `name` | Field name of record to display in the suggestion item or function which accepts the current record as argument (`record => {string}`) | +| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | +| `resettable` | Optional | `boolean` | `false` | If `true`, display a button to reset the changes in this input value | +| `translateChoice` | Optional | `boolean` | `true` | Whether the choices should be translated | `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -1937,17 +1696,13 @@ Set the `choices` attribute to determine the options (with `id`, `name` tuples): ```jsx import { SelectArrayInput } from 'react-admin'; -; + ``` You can also customize the properties to use for the option name and value, @@ -1955,44 +1710,35 @@ thanks to the `optionText` and `optionValue` attributes. ```jsx const choices = [ - { _id: '1', name: 'Book', plural_name: 'Books' }, - { _id: '2', name: 'Video', plural_name: 'Videos' }, - { _id: '3', name: 'Audio', plural_name: 'Audios' }, + { _id: '1', name: 'Book', plural_name: 'Books' }, + { _id: '2', name: 'Video', plural_name: 'Videos' }, + { _id: '3', name: 'Audio', plural_name: 'Audios' }, ]; -; + ``` `optionText` also accepts a function, so you can shape the option text at will: ```jsx const choices = [ - { id: '1', name: 'Book', quantity: 23 }, - { id: '2', name: 'Video', quantity: 56 }, - { id: '3', name: 'Audio', quantity: 12 }, + { id: '1', name: 'Book', quantity: 23 }, + { id: '2', name: 'Video', quantity: 56 }, + { id: '3', name: 'Audio', quantity: 12 }, ]; const optionRenderer = choice => `${choice.name} (${choice.quantity})`; -; + ``` The choices are translated by default, so you can use translation identifiers as choices: ```js const choices = [ - { id: 'books', name: 'myroot.category.books' }, - { id: 'sport', name: 'myroot.category.sport' }, + { id: 'books', name: 'myroot.category.books' }, + { id: 'sport', name: 'myroot.category.sport' }, ]; ``` -You can render any item as disabled by setting its `disabled` property to `true`: +You can render any item as disabled by setting its `disabled` property to `true`: ```jsx const choices = [ @@ -2000,15 +1746,10 @@ const choices = [ { _id: 456, full_name: 'Jane Austen', sex: 'F' }, { _id: 1, full_name: 'System Administrator', sex: 'F', disabled: true }, ]; -; + ``` -You can use a custom field name by setting the `disableValue` prop: +You can use a custom field name by setting the `disableValue` prop: ```jsx const choices = [ @@ -2016,23 +1757,15 @@ const choices = [ { _id: 456, full_name: 'Jane Austen', sex: 'F' }, { _id: 987, full_name: 'Jack Harden', sex: 'M', not_available: true }, ]; -; + ``` Lastly, use the `options` attribute if you want to override any of the ` - + + ``` @@ -2894,19 +2598,9 @@ const ItemEdit = props => ( ```jsx const LatLongInput = () => ( - +   - + ); ``` @@ -2923,19 +2617,9 @@ import { Labeled } from 'react-admin'; const LatLngInput = () => ( - +   - + ); @@ -2947,8 +2631,8 @@ Now the component will render with a label: ```html - - + + ``` @@ -2964,7 +2648,7 @@ import { useField } from 'react-final-form'; const BoundedTextField = ({ name, label }) => { const { input: { onChange }, - meta: { touched, error }, + meta: { touched, error } } = useField(name); return ( { const { input: { name, onChange, ...rest }, meta: { touched, error }, - isRequired, + isRequired } = useInput(props); return ( @@ -3039,23 +2723,13 @@ const BoundedTextField = props => { ); }; const LatLngInput = props => { - const { source, ...rest } = props; + const {source, ...rest} = props; return ( - +   - + ); }; @@ -3072,11 +2746,14 @@ import { useInput } from 'react-admin'; const SexInput = props => { const { input, - meta: { touched, error }, + meta: { touched, error } } = useInput(props); return ( - Male Female @@ -3088,20 +2765,16 @@ export default SexInput; **Tip**: `useInput` accepts all arguments that you can pass to `useField`. That means that components using `useInput` accept props like [format](https://final-form.org/docs/react-final-form/types/FieldProps#format) and [parse](https://final-form.org/docs/react-final-form/types/FieldProps#parse), to convert values from the form to the input, and vice-versa: ```jsx -const parse = value => { - /* ... */ -}; -const format = value => { - /* ... */ -}; +const parse = value => {/* ... */}; +const format = value => {/* ... */}; const PersonEdit = props => ( (formValue === 0 ? 'M' : 'F')} - parse={inputValue => (inputValue === 'M' ? 0 : 1)} + format={formValue => formValue === 0 ? 'M' : 'F'} + parse={inputValue => inputValue === 'M' ? 0 : 1} /> @@ -3118,4 +2791,4 @@ You can find components for react-admin in third-party repositories. - [@bb-tech/ra-components](https://github.com/bigbasket/ra-components): `JsonInput` which allows only valid JSON as input, `JsonField` to view JSON properly on show card and `TrimField` to trim the fields while showing in `Datagrid` in `List` component. - [@react-page/react-admin](https://react-page.github.io/docs/#/integration-react-admin): ReactPage is a rich content editor and can comes with a ready-to-use React-admin input component. [check out the demo](https://react-page.github.io/examples/reactadmin) -- **DEPRECATED V3** [LoicMahieu/aor-tinymce-input](https://github.com/LoicMahieu/aor-tinymce-input): a TinyMCE component, useful for editing HTML +- **DEPRECATED V3** [LoicMahieu/aor-tinymce-input](https://github.com/LoicMahieu/aor-tinymce-input): a TinyMCE component, useful for editing HTML