import React , { useCallback , useEffect , useRef , useState , useMemo , isValidElement , } from 'react' ; import Downshift , { DownshiftProps } from 'downshift' ; import get from 'lodash/get' ; import classNames from 'classnames' ; import { TextField , InputAdornment , IconButton , InputProps , } from '@material-ui/core' ; import { makeStyles } from '@material-ui/core/styles' ; import ClearIcon from '@material-ui/icons/Clear' ; import { TextFieldProps } from '@material-ui/core/TextField' ; import { useInput , FieldTitle , ChoicesInputProps , UseChoicesOptions , mergeRefs , useSuggestions , useTranslate , warning , } from 'ra-core' ; import InputHelperText from './InputHelperText' ; import AutocompleteSuggestionList from './AutocompleteSuggestionList' ; import AutocompleteSuggestionItem from './AutocompleteSuggestionItem' ; import { AutocompleteInputLoader } from './AutocompleteInputLoader' ; import { SupportCreateSuggestionOptions , useSupportCreateSuggestion , } from './useSupportCreateSuggestion' ; example example example example example example example export const AutocompleteInput = ( props ) => { const { allowEmpty , className , classes : classesOverride , clearAlwaysVisible , choices = [ ] , createLabel , createItemLabel , createValue , create , disabled , emptyText , emptyValue , format , fullWidth , helperText , id : idOverride , input : inputOverride , inputText , isRequired : isRequiredOverride , label , limitChoicesToValue , loaded , loading , margin = 'dense' , matchSuggestion , meta : metaOverride , onBlur , onChange , onCreate , onFocus , options : { suggestionsContainerProps , labelProps , InputProps , ... options } = { suggestionsContainerProps : undefined , labelProps : undefined , InputProps : undefined , } , optionText = 'name' , optionValue = 'id' , parse , refetch , resettable , resource , setFilter , shouldRenderSuggestions : shouldRenderSuggestionsOverride , source , suggestionLimit , translateChoice = true , validate , variant = 'filled' , ... rest } = props ; if ( isValidElement ( optionText ) && ! inputText ) { throw new Error ( `If the optionText prop is a React element, you must also specify the inputText prop:
        <AutocompleteInput
            inputText={(record) => record.title}
        />` ) ; } warning ( isValidElement ( optionText ) && ! matchSuggestion , `If the optionText prop is a React element, you must also specify the matchSuggestion prop:
<AutocompleteInput
    matchSuggestion={(filterValue, suggestion) => true}
/>
        ` ) ; warning ( source === undefined , `If you're not wrapping the AutocompleteInput inside a ReferenceInput, you must provide the source prop` ) ; warning ( choices === undefined , `If you're not wrapping the AutocompleteInput inside a ReferenceInput, you must provide the choices prop` ) ; const classes = useStyles ( props ) ; let inputEl = useRef < HTMLInputElement > ( ) ; let anchorEl = useRef < any > ( ) ; const translate = useTranslate ( ) ; const { id , input , isRequired , meta : { touched , error , submitError } , } = useInput ( { format , id : idOverride , input : inputOverride , meta : metaOverride , onBlur , onChange , onFocus , parse , resource , source , validate , ... rest , } ) ; const [ filterValue , setFilterValue ] = useState ( '' ) ; const getSuggestionFromValue = useCallback ( value => choices . find ( choice => get ( choice , optionValue ) === value ) , [ choices , optionValue ] ) ; const selectedItem = useMemo ( ( ) => getSuggestionFromValue ( input . value ) || null , [ input . value , getSuggestionFromValue ] ) ; const doesQueryMatchSuggestion = useMemo ( ( ) => { if ( isValidElement ( optionText ) ) { return choices . some ( choice => matchSuggestion ( filterValue , choice ) ) ; } const isFunction = typeof optionText === 'function' ; if ( isFunction ) { const hasOption = choices . some ( choice => { const text = optionText ( choice ) as string ; return get ( choice , text ) === filterValue ; } ) ; const selectedItemText = optionText ( selectedItem ) ; return hasOption || selectedItemText === filterValue ; } const selectedItemText = get ( selectedItem , optionText ) ; const hasOption = choices . some ( choice => { return get ( choice , optionText ) === filterValue ; } ) ; return selectedItemText === filterValue || hasOption ; } , [ choices , optionText , filterValue , matchSuggestion , selectedItem ] ) ; const shouldAllowCreate = ! doesQueryMatchSuggestion ; const { getChoiceText , getChoiceValue , getSuggestions } = useSuggestions ( { allowEmpty , choices , emptyText , emptyValue , limitChoicesToValue , matchSuggestion , optionText , optionValue , selectedItem , suggestionLimit , translateChoice , } ) ; const handleChange = useCallback ( async ( item , newItem ) => { const value = getChoiceValue ( newItem || item ) ; if ( value == null && filterValue ) { setFilterValue ( '' ) ; } input . onChange ( value ) ; } , [ filterValue , getChoiceValue , input ] ) ; const { getCreateItem , handleChange : handleChangeWithCreateSupport , createElement , } = useSupportCreateSuggestion ( { create , createLabel , createItemLabel , createValue , handleChange , filter : filterValue , onCreate , optionText , } ) ; const handleFilterChange = useCallback ( ( eventOrValue ) => { const event = eventOrValue as React . ChangeEvent < { value : string ; } > ; const value = event . target ? event . target . value : ( eventOrValue as string ) ; if ( setFilter ) { setFilter ( value ) ; } } , [ setFilter ] ) ; useEffect ( ( ) => { handleFilterChange ( '' ) ; setFilterValue ( typeof input . value === 'undefined' || input . value === null || selectedItem === null ? '' : inputText ? inputText ( getChoiceText ( selectedItem ) . props . record ) : getChoiceText ( selectedItem ) ) ; } , [ input . value , handleFilterChange , selectedItem , getChoiceText , inputText , ] ) ; useEffect ( ( ) => { inputEl . current . blur ( ) ; } , [ input . value ] ) ; const updateAnchorEl = ( ) => { if ( ! inputEl . current ) { return ; } const inputPosition = inputEl . current . getBoundingClientRect ( ) as DOMRect ; if ( ! anchorEl . current ) { anchorEl . current = { getBoundingClientRect : ( ) => inputPosition } ; } else { const anchorPosition = anchorEl . current . getBoundingClientRect ( ) ; if ( anchorPosition . x !== inputPosition . x || anchorPosition . y !== inputPosition . y ) { anchorEl . current = { getBoundingClientRect : ( ) => inputPosition , } ; } } } ; const storeInputRef = input => { inputEl . current = input ; updateAnchorEl ( ) ; } ; const handleBlur = useCallback ( event => { handleFilterChange ( '' ) ; setFilterValue ( input . value ? inputText ? inputText ( getChoiceText ( selectedItem ) . props . record ) : getChoiceText ( selectedItem ) : '' ) ; input . onBlur ( event ) ; } , [ getChoiceText , handleFilterChange , input , inputText , selectedItem ] ) ; const handleFocus = useCallback ( openMenu => event => { openMenu ( event ) ; input . onFocus ( event ) ; } , [ input ] ) ; const shouldRenderSuggestions = val => { if ( shouldRenderSuggestionsOverride !== undefined && typeof shouldRenderSuggestionsOverride === 'function' ) { return shouldRenderSuggestionsOverride ( val ) ; } return true ; } ; const { endAdornment , inputRef , ... InputPropsWithoutEndAdornment } = InputProps || { } ; const handleClickClearButton = useCallback ( openMenu => event => { event . stopPropagation ( ) ; setFilterValue ( '' ) ; input . onChange ( '' ) ; openMenu ( event ) ; input . onFocus ( event ) ; } , [ input ] ) ; const getEndAdornment = openMenu => { if ( ! resettable ) { if ( endAdornment ) { return endAdornment ; } if ( loading ) { return < AutocompleteInputLoader />; } } else if ( ! filterValue ) { const label = translate ( 'ra.action.clear_input_value' ) ; if ( clearAlwaysVisible ) { return ( < InputAdornment position = "end" > < IconButton className = { classes . clearButton } aria - label = { label } title = { label } disableRipple disabled = { true } > < ClearIcon className = { classNames ( classes . clearIcon , classes . visibleClearIcon ) } /> < /IconButton> { loading && < AutocompleteInputLoader />} < /InputAdornment>); } else { if ( endAdornment ) { return endAdornment ; } else { return ( < InputAdornment position = "end" > < span className = { classes . clearButton } > & nbsp ; < /span> { loading && < AutocompleteInputLoader />} < /InputAdornment>); } } } else { const label = translate ( 'ra.action.clear_input_value' ) ; return ( < InputAdornment position = "end" > < IconButton className = { classes . clearButton } aria - label = { label } title = { label } disableRipple onClick = { handleClickClearButton ( openMenu ) } onMouseDown = { handleMouseDownClearButton } disabled = { disabled } > < ClearIcon className = { classNames ( classes . clearIcon , { [ classes . visibleClearIcon ] : clearAlwaysVisible || filterValue , } ) } /> < /IconButton> { loading && < AutocompleteInputLoader />} < /InputAdornment>); } } ; return ( < > < Downshift inputValue = { filterValue } onChange = { handleChangeWithCreateSupport } selectedItem = { selectedItem } itemToString = { item => getChoiceValue ( item ) } { ... rest } > { ( { getInputProps , getItemProps , getLabelProps , getMenuProps , isOpen , highlightedIndex , openMenu , } ) => { const isMenuOpen = isOpen && shouldRenderSuggestions ( filterValue ) ; const { id : downshiftId , value , onBlur , onChange , onFocus , ref , size , color , ... inputProps } = getInputProps ( { onBlur : handleBlur , onFocus : handleFocus ( openMenu ) , ... InputProps , } ) ; const suggestions = [ ... getSuggestions ( filterValue ) , ... ( ( onCreate || create ) && shouldAllowCreate ? [ getCreateItem ( ) ] : [ ] ) , ] ; return ( < div className = { classes . container } > < TextField id = { id } name = { input . name } InputProps = { { inputRef : mergeRefs ( [ storeInputRef , inputRef , ] ) , endAdornment : getEndAdornment ( openMenu ) , onBlur , onChange : event => { setFilterValue ( event . target . value ) ; handleFilterChange ( event ) ; onChange ! ( event as React . ChangeEvent < HTMLInputElement > ) ; } , onFocus , ... InputPropsWithoutEndAdornment , } } error = { ! ! ( touched && ( error || submitError ) ) } label = { < FieldTitle label = { label } { ... labelProps } source = { source } resource = { resource } isRequired = { typeof isRequiredOverride !== 'undefined' ? isRequiredOverride : isRequired } / > } InputLabelProps = { getLabelProps ( { htmlFor : id , } ) } helperText = { < InputHelperText touched = { touched } error = { error || submitError } helperText = { helperText } / > } disabled = { disabled } variant = { variant } margin = { margin } fullWidth = { fullWidth } value = { filterValue } className = { className } size = { size as any } color = { color as any } { ... inputProps } { ... options } /> < AutocompleteSuggestionList isOpen = { isMenuOpen } menuProps = { getMenuProps ( { } , { suppressRefError : true } ) } inputEl = { inputEl . current } suggestionsContainerProps = { suggestionsContainerProps } className = { classes . suggestionsContainer } > { suggestions . map ( ( suggestion , index ) => ( < AutocompleteSuggestionItem key = { getChoiceValue ( suggestion ) } suggestion = { suggestion } index = { index } highlightedIndex = { highlightedIndex } isSelected = { input . value === getChoiceValue ( suggestion ) } filterValue = { filterValue } getSuggestionText = { getChoiceText } createValue = { createValue } { ... getItemProps ( { item : suggestion , } ) } / > ) ) } < /AutocompleteSuggestionList> < /div>); } } < /Downshift> { createElement } < />); } ; const handleMouseDownClearButton = event => { event . preventDefault ( ) ; } ; const useStyles = makeStyles ( theme => ( { container : { flexGrow : 1 , position : 'relative' , } , clearIcon : { height : 16 , width : 0 , } , visibleClearIcon : { width : 16 , } , clearButton : { height : 24 , width : 24 , padding : 0 , } , selectAdornment : { position : 'absolute' , right : 24 , } , inputAdornedEnd : { paddingRight : 0 , } , suggestionsContainer : { zIndex : theme . zIndex . modal , } , } ) , { name : 'RaAutocompleteInput' } ) ; interface Options { InputProps ? : InputProps ; labelProps ? : any ; suggestionsContainerProps ? : any ; } export interface AutocompleteInputProps extends ChoicesInputProps < TextFieldProps > , UseChoicesOptions , Omit < SupportCreateSuggestionOptions , 'handleChange' | 'optionText' > , Omit < DownshiftProps < any > , 'onChange' > { clearAlwaysVisible ? : boolean ; resettable ? : boolean ; loaded ? : boolean ; loading ? : boolean ; options ? : Options ; }