Skip to content

Instantly share code, notes, and snippets.

@jgoux
Last active September 11, 2023 15:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jgoux/f21fc6f6676deaa8789c69685fc9ede7 to your computer and use it in GitHub Desktop.
Save jgoux/f21fc6f6676deaa8789c69685fc9ede7 to your computer and use it in GitHub Desktop.
Material UI Autocomplete
import React, { Component } from 'react'
import { findDOMNode } from 'react-dom'
import R from 'ramda'
import injectSheet from 'react-jss'
import Tether from 'react-tether'
import { List } from 'react-virtualized'
const defaultState = {
input: {
searchText: ''
},
datalist: {
displayed: false,
optionFocused: 0
}
}
const defaultListStyles = {
AutocompleteList: {
border: '1px solid #d9d9d9',
backgroundColor: 'white'
}
}
const DefaultList = injectSheet(
defaultListStyles
)(({ classes, sheet, children, ...props }) =>
<div className={classes.AutocompleteList} {...props}>{children}</div>
)
const defaultOptionStyles = {
AutocompleteOption: {
cursor: 'pointer',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
// works with react-jss 6.0.0
backgroundColor: ({ focused }) => (focused ? 'rgba(0,0,0,0.1)' : 'white'),
'&:hover': {
backgroundColor: 'rgba(0,0,0,0.1)'
}
}
}
const DefaultOption = injectSheet(
defaultOptionStyles
)(({ classes, sheet, focused, children, ...props }) =>
<div className={classes.AutocompleteOption} {...props}>{children}</div>
)
const defaultProps = {
inputSearchText: ({ text }) => text,
inputRender: props => <input type="text" {...props} />,
onFocus: () => {},
onBlur: () => {},
datalistFilter: searchText => ({ text }) =>
searchText === '' || text.toLowerCase().includes(searchText.toLowerCase()),
datalistOffset: '0 0',
datalistDisplayCount: 5,
datalistRender: DefaultList,
datalistOptionHeight: 20,
datalistOptionWidth: null,
datalistOptionRender: ({ value, focused, onChange }) => {
return (
<DefaultOption
focused={focused}
onMouseDown={e => {
e.preventDefault()
}}
onMouseUp={() => {
onChange(value)
}}
>
{value.text}
</DefaultOption>
)
},
datalistNotFoundRender: ({ searchText }) => () =>
<DefaultOption>No results for {searchText}</DefaultOption>
}
// helpers to view/set state
const lenses = {
input: {
searchText: R.lensPath(['input', 'searchText'])
},
datalist: {
displayed: R.lensPath(['datalist', 'displayed']),
optionFocused: R.lensPath(['datalist', 'optionFocused'])
}
}
// state transitions
const input = {
setSearchText: value => R.set(lenses.input.searchText, value)
}
const datalist = {
show: R.set(lenses.datalist.displayed, true),
hide: R.set(lenses.datalist.displayed, false),
resetOptionFocused: R.set(
lenses.datalist.optionFocused,
defaultState.datalist.optionFocused
),
focusPreviousOption: count =>
R.over(
lenses.datalist.optionFocused,
i => ((i - 1) % count + count) % count
),
focusNextOption: count =>
R.over(
lenses.datalist.optionFocused,
i => ((i + 1) % count + count) % count
)
}
// TODO: we can do better!
const computeDatalistHeight = (count, displayCount, optionHeight) =>
count === 0
? optionHeight
: count * optionHeight < displayCount * optionHeight
? count * optionHeight
: displayCount * optionHeight
class Autocomplete extends Component {
constructor(props) {
super(props)
this.state = props.value
? input.setSearchText(props.inputSearchText(props.value))(defaultState)
: defaultState
}
componentWillReceiveProps = nextProps => {
if (this.props.value !== nextProps.value) {
const searchText = nextProps.value
? this.props.inputSearchText(nextProps.value)
: ''
this.setState(input.setSearchText(searchText))
}
}
registerDOM = root => {
this.rootDOM = findDOMNode(root)
this.inputDOM = this.rootDOM.tagName === 'INPUT'
? this.rootDOM
: this.rootDOM.getElementsByTagName('input')[0]
}
// TODO: prevent the body to scroll while the datalist is scrolled with the mousewheel
// level 1: document.body.style.overflowY = 'hidden'
// level over 9000: https://github.com/angular/material/blob/e06284aa4745dd7323a872a0a9290fd026747e30/src/core/util/util.js#L278-L317
onInputFocus = () => {
this.setState(datalist.show)
this.props.onFocus()
}
onInputChange = e => {
const searchText = e.target.value
this.setState(
R.compose(datalist.resetOptionFocused, input.setSearchText(searchText))
)
}
onInputKeyDown = values => e => {
if (values.length === 0) {
return
}
switch (e.key) {
case 'ArrowUp':
this.setState(datalist.focusPreviousOption(values.length))
break
case 'ArrowDown':
this.setState(datalist.focusNextOption(values.length))
break
case 'Enter':
const value = values[this.state.datalist.optionFocused]
const searchText = this.props.inputSearchText(value)
this.setState(
R.compose(
datalist.resetOptionFocused,
datalist.hide,
input.setSearchText(searchText)
)
)
this.props.onChange(value)
break
case 'Escape':
this.setState(R.compose(datalist.resetOptionFocused, datalist.hide))
break
default:
if (this.state.datalist.displayed === false) {
this.setState(datalist.show)
}
}
}
onInputBlur = () => {
const searchText = this.props.value
? this.props.inputSearchText(this.props.value)
: ''
this.setState(
R.compose(
datalist.hide,
datalist.resetOptionFocused,
input.setSearchText(searchText)
)
)
this.props.onBlur()
}
onDatalistChange = value => {
const searchText = this.props.inputSearchText(value)
this.setState(R.compose(input.setSearchText(searchText), datalist.hide))
this.props.onChange(value)
}
render = () => {
const {
state,
props,
rootDOM,
registerDOM,
onInputFocus,
onInputChange,
onInputKeyDown,
onInputBlur,
onDatalistChange
} = this
const valuesFiltered = props.values.filter(
props.datalistFilter(state.input.searchText)
)
const rowRenderer = ({ key, index, style }) =>
React.cloneElement(
props.datalistOptionRender({
focused: index === state.datalist.optionFocused,
searchText: state.input.searchText,
onChange: onDatalistChange,
value: valuesFiltered[index]
}),
{
key,
style: { ...style, pointerEvents: 'auto' }
}
)
const datalistHeight = computeDatalistHeight(
valuesFiltered.length,
props.datalistDisplayCount,
props.datalistOptionHeight
)
return (
<Tether
attachment="top center"
constraints={[
{
to: 'window',
attachment: 'together'
}
]}
offset={props.datalistOffset}
style={{ zIndex: '10' }}
>
{props.inputRender({
value: state.input.searchText,
ref: registerDOM,
onFocus: onInputFocus,
onBlur: onInputBlur,
onChange: onInputChange,
onKeyDown: onInputKeyDown(valuesFiltered),
className: props.className
})}
{state.datalist.displayed &&
<props.datalistRender>
<List
width={
props.datalistOptionWidth
? props.datalistOptionWidth
: rootDOM.offsetWidth
}
height={datalistHeight}
rowCount={valuesFiltered.length}
rowHeight={props.datalistOptionHeight}
rowRenderer={rowRenderer}
noRowsRenderer={props.datalistNotFoundRender({
searchText: state.input.searchText
})}
scrollToIndex={state.datalist.optionFocused}
tabIndex={null}
/>
</props.datalistRender>}
</Tether>
)
}
}
Autocomplete.defaultProps = defaultProps
export default Autocomplete
import React from 'react'
import TextField from 'material-ui/TextField'
import { MenuItem } from 'material-ui/Menu'
import Paper from 'material-ui/Paper'
import IconButton from 'material-ui/IconButton'
import ArrowDropDownIcon from 'material-ui-icons/ArrowDropDown'
import CancelIcon from 'material-ui-icons/Cancel'
import styled from 'styled-components'
import Autocomplete from './Autocomplete'
const Highlight = styled.span`
font-weight: bold;
margin-left: 0.5ch;
`
const MuiAutocomplete = styled(({ label = '', placeholder, ...props }) =>
<Autocomplete
inputRender={({ ref, ...props }) =>
<TextField
innerRef={ref}
{...props}
label={label}
placeholder={placeholder}
/>}
datalistRender={({ children, ...props }) =>
<Paper {...props} square>{children}</Paper>}
datalistOptionHeight={48}
datalistOptionRender={({ value, focused, onChange }) =>
<MenuItem
selected={focused}
onMouseDown={e => {
e.preventDefault()
}}
onMouseUp={() => {
onChange(value)
}}
>
{value.text}
</MenuItem>}
datalistNotFoundRender={({ searchText }) => () =>
<MenuItem>
Aucun résultat pour <Highlight>{searchText}</Highlight>
</MenuItem>}
{...props}
/>
)``
const DropDownIcon = styled(ArrowDropDownIcon)`
color: ${({ theme }) => theme.palette.input.bottomLine}
`
const ClearButton = styled(props =>
<IconButton {...props}>
<CancelIcon />
</IconButton>
)`
color: ${({ theme }) => theme.palette.input.bottomLine}
`
const View = styled.div`
/*margin-top: -18px;*/
position: relative;
display: flex;
& > ${MuiAutocomplete} {
width: 100%;
}
& > ${DropDownIcon} {
position: absolute;
right: 0px;
top: 17px;
}
& > ${ClearButton} {
position: absolute;
right: -14px;
top: 7px;
}
`
const ResetableMuiAutocomplete = ({ onChange, className, value, ...props }) =>
<View className={className}>
<MuiAutocomplete value={value} onChange={onChange} {...props} />
{value
? <ClearButton
onClick={() => {
onChange({ value: null })
}}
/>
: <DropDownIcon />}
</View>
export default ResetableMuiAutocomplete
@Shrilathap
Copy link

May i know how to clear the textfield in autocomplete using external button

@jgoux
Copy link
Author

jgoux commented Sep 11, 2023

Hello @Shrilathap, sorry this code is from 2017 I totally forgot the context about it. Material-UI evolved dramatically since then, and they have a built-in autocomplete component now: https://mui.com/material-ui/react-autocomplete/ 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment