Skip to content

Commit

Permalink
Merge pull request marmelab#6971 from marmelab/mui-autocomplete-array
Browse files Browse the repository at this point in the history
Migrate AutocompleteArrayInput to MUI Autocomplete
  • Loading branch information
fzaninotto authored Jan 3, 2022
2 parents e7ac692 + 1ef0bf6 commit 842b005
Show file tree
Hide file tree
Showing 19 changed files with 914 additions and 1,285 deletions.
7 changes: 5 additions & 2 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1186,11 +1186,12 @@ test('MyComponent', () => {
});
```

## AutocompleteInput Now Uses Material UI Autocomplete
## AutocompleteInput and AutocompleteArrayInput Now Use Material UI Autocomplete

We migrated the `AutocompleteInput` so that it leverages Material UI [`<Autocomplete>`](https://mui.com/components/autocomplete/). If you relied on [Downshift](https://www.downshift-js.com/) options, you'll have to update your component.
We migrated both the `AutocompleteInput` and `AutocompleteArrayInput` components so that they leverage Material UI [`<Autocomplete>`](https://mui.com/components/autocomplete/). If you relied on [Downshift](https://www.downshift-js.com/) options, you'll have to update your component.

Besides, some props supported by the previous implementation aren't anymore:
- `allowDuplicates`: This is not supported by MUI Autocomplete.
- `clearAlwaysVisible`: the clear button is now always visible, either while hovering the input or when it has focus. You can hide it using the `<Autocomplete>` `disableClearable` prop though.
- `resettable`: Removed for the same reason as `clearAlwaysVisible`

Expand Down Expand Up @@ -2461,6 +2462,8 @@ Besides, some props which were applicable to both components did not make sense
/>
```

Finally, both the `<AutocompleteInput>` and the `<AutocompleteArrayInput>` don't need react-admin specific styles anymore so we removed the theme keys for them: `RaAutocompleteInput` and `RaAutocompleteArrayInput`. To customize their styles, you can either use the [sx](https://mui.com/system/the-sx-prop/#main-content) prop or add a `MuiAutocomplete` key in your [theme](https://mui.com/customization/theme-components/#global-style-overrides).

## New DataProviderContext Requires Custom App Modification

The new dataProvider-related hooks (`useQuery`, `useMutation`, `useDataProvider`, etc.) grab the `dataProvider` instance from a new React context. If you use the `<Admin>` component, your app will continue to work and there is nothing to do, as `<Admin>` now provides that context. But if you use a Custom App, you'll need to set the value of that new `DataProvider` context:
Expand Down
29 changes: 17 additions & 12 deletions cypress/integration/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,27 +94,32 @@ describe('Edit Page', () => {
// Music is selected by default
cy.get(
EditPostTagsPage.elements.input('tags', 'reference-array-input')
)
.get(`div[role=button]`)
.contains('Music')
.should('exist');
).within(() => {
cy.get(`[role=button]`).contains('Music').should('exist');
});

EditPostTagsPage.clickInput('change-filter');

// Music should not be selected anymore after filter reset
cy.get(
EditPostTagsPage.elements.input('tags', 'reference-array-input')
)
.get(`div[role=button]`)
.contains('Music')
.should('not.exist');
).within(() => {
cy.get(`[role=button]`).should('not.exist');
});

EditPostTagsPage.clickInput('tags', 'reference-array-input');
cy.get(
EditPostTagsPage.elements.input('tags', 'reference-array-input')
).within(() => {
cy.get(`input`).click();
});

// Music should not be visible in the list after filter reset
cy.get('div[role=listbox]').contains('Music').should('not.exist');

cy.get('div[role=listbox]').contains('Photo').should('exist');
cy.get('[role="listbox"]').within(() => {
cy.contains('Music').should('not.exist');
});
cy.get('[role="listbox"]').within(() => {
cy.contains('Photo').should('exist');
});
});
});

Expand Down
2 changes: 1 addition & 1 deletion cypress/support/EditPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default url => ({
return `.ra-input-${name} label`;
}
if (type === 'reference-array-input') {
return `.ra-input div[role=combobox] input`;
return `.ra-input div[role=combobox]`;
}
return `.edit-page [name='${name}']`;
},
Expand Down
49 changes: 8 additions & 41 deletions docs/Inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -642,18 +642,14 @@ In that case, set the `translateChoice` prop to `false`.
<AutocompleteInput source="gender" choices={choices} translateChoice={false}/>
```
If you want to limit the initial choices shown to the current value only, you can set the `limitChoicesToValue` prop.
When dealing with a large amount of `choices` you may need to limit the number of suggestions that are rendered in order to maintain usable performance. The `shouldRenderSuggestions` is an optional prop that allows you to set conditions on when to render suggestions. An easy way to improve performance would be to skip rendering until the user has entered 2 or 3 characters in the search box. This lowers the result set significantly, and might be all you need (depending on your data set).
Ex. `<AutocompleteInput shouldRenderSuggestions={(val) => { return val.trim().length > 2 }} />` would not render any suggestions until the 3rd character has been entered. This prop is passed to the underlying `react-autosuggest` component and is documented [here](https://github.com/moroshko/react-autosuggest#should-render-suggestions-prop).
`<AutocompleteInput>` renders a [material-ui `<TextField>` component](https://material-ui.com/api/text-field/). Use the `options` attribute to override any of the `<TextField>` attributes:
`<AutocompleteInput>` renders a [material-ui `<Autocomplete>` component](https://mui.com/components/autocomplete/) and it accepts the `<Autocomplete>` props:
{% raw %}
```jsx
<AutocompleteInput source="category" options={{
color: 'secondary',
}} />
<AutocompleteInput source="category" size="large" />
```
{% endraw %}
Expand Down Expand Up @@ -789,12 +785,7 @@ const CreateCategory = () => {
#### CSS API
| Rule name | Description |
| ---------------------- | ------------------------------------ |
| `container` | Applied to the root element |
| `suggestionsContainer` | Applied to the suggestions container |
To override the style of all instances of `<AutocompleteInput>` using the [material-ui style overrides](https://material-ui.com/customization/globals/#css), use the `RaAutocompleteInput` key.
This component doesn't apply any custom styles on top of [material-ui `<Autocomplete>` component](https://mui.com/components/autocomplete/). Refer to their documentation to know its CSS API.
### `<RadioButtonGroupInput>`
Expand Down Expand Up @@ -1290,7 +1281,7 @@ import { ArrayInput, SimpleFormIterator, DateInput, TextInput, FormDataConsumer
### `<AutocompleteArrayInput>`
To let users choose multiple values in a list using a dropdown with autocompletion, use `<AutocompleteArrayInput>`.
It renders using [downshift](https://github.com/downshift-js/downshift) and a `fuzzySearch` filter.
It renders using Material UI [Autocomplete](https://mui.com/components/autocomplete/).
![AutocompleteArrayInput](./img/autocomplete-array-input.gif)
Expand All @@ -1311,12 +1302,12 @@ import { AutocompleteArrayInput } from 'react-admin';
| 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 |
| `inputText` | Optional | `Function` | `-` | Required if `optionText` is a custom Component, this function must return the text displayed for the current selection. |
| `matchSuggestion` | Optional | `Function` | - | Required if `optionText` is a React element. Function returning a boolean indicating whether a choice matches the filter. `(filter, choice) => boolean` |
| `onCreate` | Optional | `Function` | `-` | A function called with the current filter value when users choose to create a new choice. |
| `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value |
Expand Down Expand Up @@ -1411,13 +1402,11 @@ However, in some cases (e.g. inside a `<ReferenceInput>`), you may not want the
When dealing with a large amount of `choices` you may need to limit the number of suggestions that are rendered in order to maintain usable performance. The `shouldRenderSuggestions` is an optional prop that allows you to set conditions on when to render suggestions. An easy way to improve performance would be to skip rendering until the user has entered 2 or 3 characters in the search box. This lowers the result set significantly, and might be all you need (depending on your data set).
Ex. `<AutocompleteArrayInput shouldRenderSuggestions={(val) => { return val.trim().length > 2 }} />` would not render any suggestions until the 3rd character has been entered. This prop is passed to the underlying `react-autosuggest` component and is documented [here](https://github.com/moroshko/react-autosuggest#should-render-suggestions-prop).
Lastly, `<AutocompleteArrayInput>` renders a [material-ui `<TextField>` component](https://material-ui.com/api/text-field/). Use the `options` attribute to override any of the `<TextField>` attributes:
Lastly, `<AutocompleteArrayInput>` renders a [material-ui `<Autocomplete>` component](https://mui.com/components/autocomplete/) and accepts the `<Autocomplete>` props:
{% raw %}
```jsx
<AutocompleteArrayInput source="category" options={{
color: 'secondary',
}} />
<AutocompleteArrayInput source="category" limitTags={2} />
```
{% endraw %}
Expand All @@ -1433,17 +1422,6 @@ import { AutocompleteArrayInput, ReferenceArrayInput } from 'react-admin';
</ReferenceArrayInput>
```
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
<AutocompleteArrayInput source="category" options={{
suggestionsContainerProps: {
disablePortal: true,
}}} />
```
{% endraw %}
**Tip**: `<ReferenceArrayInput>` 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>`](#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 `<AutocompleteInput>` has only a capital A, while material-ui's `<AutoComplete>` has a capital A and a capital C. Don't mix up the components!
Expand Down Expand Up @@ -1568,18 +1546,7 @@ const CreateTag = () => {
#### CSS API
| Rule name | Description |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `container` | Applied to the container of the underlying Material UI's `TextField` component input |
| `suggestionsContainer` | Applied to the suggestions container |
| `chip` | Applied to each Material UI's `Chip` component used as selected item |
| `chipContainerFilled` | Applied to each container of each Material UI's `Chip` component used as selected item when `variant` prop is `filled` |
| `chipContainerOutlined` | Applied to each container of each `Chip` component used as selected item when `variant` prop is `outlined` |
| `inputRoot` | Styles pass as the `root` class of the underlying Material UI's `TextField` component input |
| `inputRootFilled` | Styles pass as the `root` class of the underlying Material UI's `TextField` component input when `variant` prop is `filled` |
| `inputInput` | Styles pass as the `input` class of the underlying Material UI's `TextField` component input |
To override the style of all instances of `<AutocompleteArrayInput>` using the [material-ui style overrides](https://material-ui.com/customization/globals/#css), use the `RaAutocompleteArrayInput` key.
This component doesn't apply any custom styles on top of [material-ui `<Autocomplete>` component](https://mui.com/components/autocomplete/). Refer to their documentation to know its CSS API.
### `<CheckboxGroupInput>`
Expand Down
47 changes: 45 additions & 2 deletions examples/simple/src/dataProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,52 @@
import fakeRestProvider from 'ra-data-fakerest';

import { DataProvider } from 'react-admin';
import get from 'lodash/get';
import data from './data';
import addUploadFeature from './addUploadFeature';

const dataProvider = fakeRestProvider(data, true);
const uploadCapableDataProvider = addUploadFeature(dataProvider);

const addTagsSearchSupport = (dataProvider: DataProvider) => ({
...dataProvider,
getList: (resource, params) => {
if (resource === 'tags') {
const matchSearchFilter = Object.keys(params.filter).find(key =>
key.endsWith('_q')
);
if (matchSearchFilter) {
const searchRegExp = new RegExp(
params.filter[matchSearchFilter],
'i'
);

return dataProvider.getList(resource, {
...params,
filter: item => {
const matchPublished =
item.published == params.filter.published; // eslint-disable-line eqeqeq

const fieldName = matchSearchFilter.replace(
/(_q)$/,
''
);

return (
matchPublished &&
get(item, fieldName).match(searchRegExp) !== null
);
},
});
}
}

return dataProvider.getList(resource, params);
},
});

const uploadCapableDataProvider = addUploadFeature(
addTagsSearchSupport(dataProvider)
);

const sometimesFailsDataProvider = new Proxy(uploadCapableDataProvider, {
get: (target, name) => (resource, params) => {
// set session_ended=true in localStorage to trigger an API auth error
Expand All @@ -27,6 +69,7 @@ const sometimesFailsDataProvider = new Proxy(uploadCapableDataProvider, {
return uploadCapableDataProvider[name](resource, params);
},
});

const delayedDataProvider = new Proxy(sometimesFailsDataProvider, {
get: (target, name) => (resource, params) =>
new Promise(resolve =>
Expand Down
30 changes: 25 additions & 5 deletions examples/simple/src/posts/TagReferenceInput.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import * as React from 'react';
import { styled } from '@mui/material/styles';
import { useState } from 'react';
import { useCallback, useState } from 'react';
import { useForm } from 'react-final-form';
import {
AutocompleteArrayInput,
ReferenceArrayInput,
useCreate,
useCreateSuggestionContext,
useLocale,
} from 'react-admin';
import {
Button,
Expand Down Expand Up @@ -36,6 +37,19 @@ const StyledDialog = styled(Dialog)({
},
});

const useTagsFilterToQuery = () => {
const locale = useLocale();
return useCallback(
(filter: string) =>
filter
? {
[`name.${locale}_q`]: filter,
}
: {},
[locale]
);
};

const TagReferenceInput = ({
...props
}: {
Expand All @@ -44,19 +58,25 @@ const TagReferenceInput = ({
label?: string;
}) => {
const { change } = useForm();
const [filter, setFilter] = useState(true);
const [filter, setFilter] = useState({ published: true });
const filterToQuery = useTagsFilterToQuery();
const locale = useLocale();

const handleAddFilter = () => {
setFilter(!filter);
setFilter(prev => ({ published: !prev.published }));
change('tags', []);
};

return (
<div className={classes.input}>
<ReferenceArrayInput {...props} filter={{ published: filter }}>
<ReferenceArrayInput
{...props}
filter={filter}
filterToQuery={filterToQuery}
>
<AutocompleteArrayInput
create={<CreateTag />}
optionText="name.en"
optionText={`name.${locale}`}
/>
</ReferenceArrayInput>
<Button
Expand Down
Loading

0 comments on commit 842b005

Please sign in to comment.