Skip to content

Commit

Permalink
Migrate Autocomplete inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
djhi committed Sep 13, 2019
1 parent 8ddf9cb commit 78e3c60
Show file tree
Hide file tree
Showing 6 changed files with 581 additions and 567 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,10 @@ describe('<AutocompleteArrayInput />', () => {
});

it('should allow customized rendering of suggesting item', () => {
const SuggestionItem = ({ record }: { record?: any }) => (
<div aria-label={record.name} />
);

const { getByLabelText } = render(
<Form
onSubmit={jest.fn()}
Expand All @@ -418,23 +422,8 @@ describe('<AutocompleteArrayInput />', () => {
{ id: 't', name: 'Technical' },
{ id: 'p', name: 'Programming' },
]}
suggestionComponent={React.forwardRef(
(
{
suggestion,
query,
isHighlighted,
...props
},
ref
) => (
<div
{...props}
ref={ref}
aria-label={suggestion.name}
/>
)
)}
optionText={<SuggestionItem />}
matchSuggestion={(filter, choice) => true}
/>
)}
/>
Expand Down
147 changes: 74 additions & 73 deletions packages/ra-ui-materialui/src/input/AutocompleteArrayInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ import React, {
useEffect,
useRef,
FunctionComponent,
useMemo,
isValidElement,
} from 'react';
import Downshift, { DownshiftProps } from 'downshift';
import classNames from 'classnames';
import get from 'lodash/get';
import { makeStyles, TextField, Chip } from '@material-ui/core';
import { TextFieldProps } from '@material-ui/core/TextField';
import { useTranslate, useInput, FieldTitle, InputProps } from 'ra-core';
import {
useInput,
FieldTitle,
InputProps,
useSuggestions,
warning,
} from 'ra-core';

import InputHelperText from './InputHelperText';
import getSuggestionsFactory from './getSuggestions';
import AutocompleteSuggestionList from './AutocompleteSuggestionList';
import AutocompleteSuggestionItem from './AutocompleteSuggestionItem';

Expand Down Expand Up @@ -56,6 +63,18 @@ interface Options {
* const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`;
* <AutocompleteArrayInput source="author_id" choices={choices} optionText={optionRenderer} />
*
* `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.
* Note that you must also specify the `matchSuggestion` prop
* @example
* const choices = [
* { id: 123, first_name: 'Leo', last_name: 'Tolstoi' },
* { id: 456, first_name: 'Jane', last_name: 'Austen' },
* ];
* const matchSuggestion = (filterValue, choice) => choice.first_name.match(filterValue) || choice.last_name.match(filterValue);
* const FullNameField = ({ record }) => <span>{record.first_name} {record.last_name}</span>;
* <SelectInput source="gender" choices={choices} optionText={<FullNameField />} matchSuggestion={matchSuggestion} />
*
* The choices are translated by default, so you can use translation identifiers as choices:
* @example
* const choices = [
Expand All @@ -79,12 +98,15 @@ const AutocompleteArrayInput: FunctionComponent<
allowEmpty,
classes: classesOverride,
choices = [],
emptyText,
emptyValue,
helperText,
id: idOverride,
input: inputOverride,
isRequired: isRequiredOverride,
limitChoicesToValue,
margin,
matchSuggestion,
meta: metaOverride,
onBlur,
onChange,
Expand All @@ -108,7 +130,15 @@ const AutocompleteArrayInput: FunctionComponent<
variant = 'filled',
...rest
}) => {
const translate = useTranslate();
warning(
isValidElement(optionText) && !matchSuggestion,
`If the optionText prop is a React element, you must also specify the matchSuggestion prop:
<AutocompleteInput
matchSuggestion={(filterValue, suggestion) => true}
/>
`
);

const classes = useStyles({ classes: classesOverride });

let inputEl = useRef<HTMLInputElement>();
Expand All @@ -135,6 +165,30 @@ const AutocompleteArrayInput: FunctionComponent<

const [filterValue, setFilterValue] = React.useState('');

const getSuggestionFromValue = useCallback(
value => choices.find(choice => get(choice, optionValue) === value),
[choices, optionValue]
);

const selectedItems = useMemo(
() => (input.value || []).map(getSuggestionFromValue),
[input.value, getSuggestionFromValue]
);

const { getChoiceText, getChoiceValue, getSuggestions } = useSuggestions({
allowEmpty,
choices,
emptyText,
emptyValue,
limitChoicesToValue,
matchSuggestion,
optionText,
optionValue,
selectedItem: selectedItems,
suggestionLimit,
translateChoice,
});

const handleFilterChange = useCallback(
(eventOrValue: React.ChangeEvent<{ value: string }> | string) => {
const event = eventOrValue as React.ChangeEvent<{ value: string }>;
Expand All @@ -158,34 +212,6 @@ const AutocompleteArrayInput: FunctionComponent<
handleFilterChange('');
}, [input.value, handleFilterChange]);

const getSuggestionValue = useCallback(
suggestion => get(suggestion, optionValue),
[optionValue]
);

const getSuggestionFromValue = useCallback(
value => choices.find(choice => get(choice, optionValue) === value),
[choices, optionValue]
);

const getSuggestionText = useCallback(
suggestion => {
if (!suggestion) return '';

const suggestionLabel =
typeof optionText === 'function'
? optionText(suggestion)
: get(suggestion, optionText, '');

return translateChoice
? translate(suggestionLabel, { _: suggestionLabel })
: suggestionLabel;
},
[optionText, translate, translateChoice]
);

const selectedItems = (input.value || []).map(getSuggestionFromValue);

const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
// Remove latest item from array when user hits backspace with no text
Expand All @@ -198,10 +224,10 @@ const AutocompleteArrayInput: FunctionComponent<
0,
selectedItems.length - 1
);
input.onChange(newSelectedItems.map(getSuggestionValue));
input.onChange(newSelectedItems.map(getChoiceValue));
}
},
[filterValue.length, getSuggestionValue, input, selectedItems]
[filterValue.length, getChoiceValue, input, selectedItems]
);

const handleChange = useCallback(
Expand All @@ -210,22 +236,22 @@ const AutocompleteArrayInput: FunctionComponent<
? [...selectedItems]
: [...selectedItems, item];
setFilterValue('');
input.onChange(newSelectedItems.map(getSuggestionValue));
input.onChange(newSelectedItems.map(getChoiceValue));
},
[getSuggestionValue, input, selectedItems]
[getChoiceValue, input, selectedItems]
);

const handleDelete = useCallback(
event => {
const newSelectedItems = [...selectedItems];
const value = event.target.getAttribute('data-item');
const item = choices.find(
choice => getSuggestionValue(choice) == value // eslint-disable-line eqeqeq
choice => getChoiceValue(choice) == value // eslint-disable-line eqeqeq
);
newSelectedItems.splice(newSelectedItems.indexOf(item), 1);
input.onChange(newSelectedItems.map(getSuggestionValue));
input.onChange(newSelectedItems.map(getChoiceValue));
},
[choices, getSuggestionValue, input, selectedItems]
[choices, getChoiceValue, input, selectedItems]
);

// This function ensures that the suggestion list stay aligned to the
Expand Down Expand Up @@ -256,28 +282,6 @@ const AutocompleteArrayInput: FunctionComponent<
}
};

const getSuggestions = useCallback(
getSuggestionsFactory({
choices,
allowEmpty: false, // We do not want to insert an empty choice
optionText,
optionValue,
limitChoicesToValue,
getSuggestionText,
selectedItem: selectedItems,
suggestionLimit,
}),
[
choices,
optionText,
optionValue,
limitChoicesToValue,
getSuggestionText,
input,
suggestionLimit,
]
);

const storeInputRef = input => {
inputEl.current = input;
updateAnchorEl();
Expand Down Expand Up @@ -316,7 +320,7 @@ const AutocompleteArrayInput: FunctionComponent<
inputValue={filterValue}
onChange={handleChange}
selectedItem={selectedItems}
itemToString={item => getSuggestionValue(item)}
itemToString={item => getChoiceValue(item)}
{...rest}
>
{({
Expand All @@ -332,6 +336,7 @@ const AutocompleteArrayInput: FunctionComponent<
const isMenuOpen =
isOpen && shouldRenderSuggestions(suggestionFilter);
const {
id: idFromDownshift,
onBlur,
onChange,
onFocus,
Expand All @@ -345,10 +350,9 @@ const AutocompleteArrayInput: FunctionComponent<
return (
<div className={classes.container}>
<TextField
id={id}
fullWidth
InputProps={{
id,
name: id,
inputRef: storeInputRef,
classes: {
root: classNames(classes.inputRoot, {
Expand All @@ -368,12 +372,10 @@ const AutocompleteArrayInput: FunctionComponent<
<Chip
key={index}
tabIndex={-1}
label={getSuggestionText(item)}
label={getChoiceText(item)}
className={classes.chip}
onDelete={handleDelete}
data-item={getSuggestionValue(
item
)}
data-item={getChoiceValue(item)}
/>
))}
</div>
Expand Down Expand Up @@ -427,18 +429,17 @@ const AutocompleteArrayInput: FunctionComponent<
{getSuggestions(suggestionFilter).map(
(suggestion, index) => (
<AutocompleteSuggestionItem
key={getSuggestionValue(suggestion)}
key={getChoiceValue(suggestion)}
suggestion={suggestion}
index={index}
highlightedIndex={highlightedIndex}
isSelected={selectedItems
.map(getSuggestionValue)
.map(getChoiceValue)
.includes(
getSuggestionValue(suggestion)
getChoiceValue(suggestion)
)}
inputValue={filterValue}
getSuggestionText={getSuggestionText}
component={suggestionComponent}
filterValue={filterValue}
getSuggestionText={getChoiceText}
{...getItemProps({
item: suggestion,
})}
Expand Down
Loading

0 comments on commit 78e3c60

Please sign in to comment.