Skip to content

Commit

Permalink
Merge pull request #8162 from marmelab/remove-emptyvalue
Browse files Browse the repository at this point in the history
Implement `emptyText` and `emptyValue` in `AutocompleteInput`
  • Loading branch information
slax57 authored Sep 28, 2022
2 parents 3226bcf + abf75e1 commit 99106d6
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 38 deletions.
23 changes: 21 additions & 2 deletions docs/AutocompleteInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ import { AutocompleteInput, ReferenceInput } from 'react-admin';
| `choices` | Optional | `Object[]` | `-` | List of items to autosuggest. Required if not inside a ReferenceInput. |
| `create` | Optional | `Element` | `-` | A React Element to render when users want to create a new choice |
| `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 |
| `emptyValue` | Optional | `any` | `''` | The value 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` | `-` | Required if `optionText` is a custom Component, this function must return the text displayed for the current selection. |
| `filterToQuery` | Optional | `string` => `Object` | `searchText => ({ q: [searchText] })` | How to transform the searchText into a parameter for the data provider |
| `setFilter` | Optional | `Function` | `null` | A callback to inform the `searchText` has changed and new `choices` can be retrieved based on this `searchText`. Signature `searchText => void`. This function is automatically set up when using `ReferenceInput`. |
| `setFilter` | Optional | `Function` | `null` | A callback to inform the `searchText` has changed and new `choices` can be retrieved based on this `searchText`. Signature `searchText => void`. This function is automatically set up when using `ReferenceInput`. |
| `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().length > 2` |
| `suggestionLimit` | Optional | `number` | `null` | Limits the numbers of suggestions that are shown in the dropdown list |

Expand Down Expand Up @@ -97,6 +97,25 @@ When used inside a `<ReferenceInput>`, `<AutocompleteInput>` doesn't need a `cho
See [Using in a `ReferenceInput>`](#using-in-a-referenceinput) below for more information.


## `emptyValue`

An empty choice is always added (with a default `''` value, which you can override with the `emptyValue` prop) on top of the options. You can furthermore customize the empty choice by using the `emptyText` prop, which can receive a string or a React Element.

```jsx
<AutocompleteInput
source="author_id"
emptyValue={0}
emptyText="No author"
choices={[
{ id: 123, name: 'Leo Tolstoi' },
{ id: 456, name: 'Jane Austen' },
]}
/>
```

**Note**: `emptyValue` cannot be set to `undefined` or `null` since the `dataProvider` method will receive an empty string on submit due to the nature of HTML inputs.


## `optionText`

You can customize the choice field to use for the option name, thanks to the `optionText` attribute:
Expand Down
8 changes: 5 additions & 3 deletions docs/SelectInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,18 @@ const choices = [

## `emptyValue`

An empty choice is always added (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.
An empty choice is always added (with a default `''` value, which you can override 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
<SelectInput source="category" emptyValue={null} choices={[
<SelectInput source="category" emptyValue={0} choices={[
{ id: 'programming', name: 'Programming' },
{ id: 'lifestyle', name: 'Lifestyle' },
{ id: 'photography', name: 'Photography' },
]} />
```

**Note**: `emptyValue` cannot be set to `undefined` or `null` since the `dataProvider` method will receive an empty string on submit due to the nature of HTML inputs.

## `options`

Use the `options` attribute if you want to override any of MUI's `<SelectField>` attributes:
Expand Down Expand Up @@ -364,4 +366,4 @@ const CreateCategory = () => {
);
};
```
{% endraw %}
{% endraw %}
16 changes: 2 additions & 14 deletions packages/ra-core/src/form/useSuggestions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@ import { useTranslate } from '../i18n';
*
* @param allowDuplicates A boolean indicating whether a suggestion can be added several times
* @param choices An array of available choices
* @param emptyText The text to use for the empty suggestion. Defaults to an empty string
* @param emptyValue The value to use for the empty suggestion. Defaults to `null`
* @param limitChoicesToValue A boolean indicating whether the initial suggestions should be limited to the currently selected one(s)
* @param matchSuggestion Optional unless `optionText` is a React element. Function which check whether a choice matches a filter. Must return a boolean.
* @param optionText Either a string defining the property to use to get the choice text, a function or a React element
* @param optionValue The property to use to get the choice value
* @param selectedItem The currently selected item. May be an array of selected items
* @param selectedItem The currently selected item. Maybe an array of selected items
* @param suggestionLimit The maximum number of suggestions returned
* @param translateChoice A boolean indicating whether to option text should be translated
*
Expand All @@ -28,8 +26,6 @@ export const useSuggestions = ({
choices,
createText = 'ra.action.create',
createValue = '@@create',
emptyText = '',
emptyValue = null,
limitChoicesToValue,
matchSuggestion,
optionText,
Expand All @@ -52,8 +48,6 @@ export const useSuggestions = ({
choices,
createText,
createValue,
emptyText: translate(emptyText, { _: emptyText }),
emptyValue,
getChoiceText,
getChoiceValue,
limitChoicesToValue,
Expand All @@ -68,8 +62,6 @@ export const useSuggestions = ({
choices,
createText,
createValue,
emptyText,
emptyValue,
getChoiceText,
getChoiceValue,
limitChoicesToValue,
Expand Down Expand Up @@ -98,8 +90,6 @@ export interface UseSuggestionsOptions extends UseChoicesOptions {
choices: any[];
createText?: string;
createValue?: any;
emptyText?: string;
emptyValue?: any;
limitChoicesToValue?: boolean;
matchSuggestion?: (
filter: string,
Expand Down Expand Up @@ -160,8 +150,6 @@ export const getSuggestionsFactory = ({
choices = [],
createText = 'ra.action.create',
createValue = '@@create',
emptyText = '',
emptyValue = null,
optionText = 'name',
optionValue = 'id',
getChoiceText,
Expand Down Expand Up @@ -263,7 +251,7 @@ const limitSuggestions = (suggestions: any[], limit: any = 0) =>
* [{ id: 1, name: 'foo'}, { id: 2, name: 'bar' }],
* );
*
* // Will return [{ id: null, name: '' }, { id: 1, name: 'foo' }, , { id: 2, name: 'bar' }]
* // Will return [{ id: null, name: '' }, { id: 1, name: 'foo' }, { id: 2, name: 'bar' }]
*
* @param suggestions List of suggestions
* @param options
Expand Down
29 changes: 28 additions & 1 deletion packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,33 @@ describe('<AutocompleteInput />', () => {
expect(screen.queryByDisplayValue('foo')).not.toBeNull();
});

describe('emptyText', () => {
it('should allow to have an empty menu option text by passing a string', () => {
const emptyText = 'Default';

render(
<AdminContext dataProvider={testDataProvider()}>
<SimpleForm onSubmit={jest.fn()}>
<AutocompleteInput
emptyText={emptyText}
{...defaultProps}
choices={[{ id: 2, name: 'foo' }]}
/>
</SimpleForm>
</AdminContext>
);
fireEvent.mouseDown(
screen.getByLabelText('resources.users.fields.role')
);

expect(screen.queryAllByRole('option').length).toEqual(1);

const input = screen.getByRole('textbox') as HTMLInputElement;

expect(input.value).toEqual('Default');
});
});

describe('optionValue', () => {
it('should use optionValue as value identifier', async () => {
render(
Expand Down Expand Up @@ -919,7 +946,7 @@ describe('<AutocompleteInput />', () => {
fireEvent.change(input, { target: { value: 'foo' } });
await waitFor(
() => {
expect(screen.getByRole('listbox').children).toHaveLength(1);
expect(screen.queryAllByRole('option')).toHaveLength(1);
},
{ timeout: 2000 }
);
Expand Down
77 changes: 77 additions & 0 deletions packages/ra-ui-materialui/src/input/AutocompleteInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ const dataProvider = {
update: (resource, params) => Promise.resolve(params),
} as any;

const dataProviderEmpty = {
getOne: (resource, params) =>
Promise.resolve({
data: {
id: 1,
title: 'War and Peace',
author: 1,
authorNone: 1,
authorEmpty: 1,
authorZero: 1,
summary:
"War and Peace broadly focuses on Napoleon's invasion of Russia, and the impact it had on Tsarist society. The book explores themes such as revolution, revolution and empire, the growth and decline of various states and the impact it had on their economies, culture, and society.",
year: 1869,
},
}),
update: (resource, params) => Promise.resolve(params),
} as any;

const history = createMemoryHistory({ initialEntries: ['/books/1'] });

const BookEdit = () => {
Expand Down Expand Up @@ -607,3 +625,62 @@ export const VeryLargeOptionsNumber = () => {
</Admin>
);
};

const BookEditWithEmptyText = () => {
const choices = [
{ id: 1, name: 'Leo Tolstoy' },
{ id: 2, name: 'Victor Hugo' },
{ id: 3, name: 'William Shakespeare' },
{ id: 4, name: 'Charles Baudelaire' },
{ id: 5, name: 'Marcel Proust' },
];
return (
<Edit
mutationMode="pessimistic"
mutationOptions={{
onSuccess: data => {
console.log(data);
},
}}
>
<SimpleForm>
<AutocompleteInput
label="emptyValue set to 'no-author', emptyText set to '' by default"
source="author"
choices={choices}
emptyValue="no-author"
fullWidth
/>
<AutocompleteInput
label="emptyValue set to 'none'"
source="authorNone"
choices={choices}
emptyValue="none"
emptyText="- No author - "
fullWidth
/>
<AutocompleteInput
label="emptyValue set to ''"
source="authorEmpty"
choices={choices}
emptyText="- No author - "
fullWidth
/>
<AutocompleteInput
label="emptyValue set to 0"
source="authorZero"
choices={choices}
emptyValue={0}
emptyText="- No author - "
fullWidth
/>
</SimpleForm>
</Edit>
);
};

export const EmptyText = () => (
<Admin dataProvider={dataProviderEmpty} history={history}>
<Resource name="books" edit={BookEditWithEmptyText} />
</Admin>
);
Loading

0 comments on commit 99106d6

Please sign in to comment.