Skip to content

Instantly share code, notes, and snippets.

@gregblass
Last active June 20, 2019 23:26
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 gregblass/23311e55707979bcb6037c86015bc342 to your computer and use it in GitHub Desktop.
Save gregblass/23311e55707979bcb6037c86015bc342 to your computer and use it in GitHub Desktop.
React Dropdown XHR/AJAX-based search component
import React, { Component } from 'react'
class DropdownSearch extends Component {
state = {
cursor: null,
data: [],
isLoading: false,
mouseEnabled: true,
query: this.props.query,
}
componentWillMount() {
document.addEventListener('mousedown', this.handleClick, false)
document.addEventListener('keydown', this.handleEscape, false)
document.addEventListener("mousemove", this.handleMouseMove, false)
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClick, false)
document.removeEventListener('keydown', this.handleEscape, false)
document.removeEventListener("mousemove", this.handleMouseMove, false)
}
componentDidUpdate() {
if (this.state.query != '' && this.state.data.length > 0) {
let li = $('li.active')[0]
if (typeof li != 'undefined') {
li.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
}
}
}
getData = () => {
this.setState({ isLoading: true })
$.ajax({
url: `${this.props.path}/search?q=${this.state.query}`,
dataType: 'json',
}).done((data) => {
this.setState({
cursor: 0,
isLoading: false,
data: data.data,
})
})
}
handleClick = (e) => {
// User clicked inside the dropdown or the link, so do nothing
if (this.dropdown.contains(e.target) || this.link == e.target) {
return
} else if (this.clear == e.target) {
this.setState({
data: [],
query: '',
})
setTimeout(() => {
this.search.focus()
}, 100)
// Click outside dropdown and not the link
} else {
this.setState({
data: []
})
}
}
handleEscape = (e) => {
if (event.keyCode === 27) {
this.setState({ data: [] })
}
}
handleInputChange = () => {
this.setState({
query: this.search.value
}, () => {
if (this.state.query != '') {
this.getData()
}
})
}
handleKeyDown = (e) => {
const { cursor, data } = this.state
if (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Enter') {
e.preventDefault()
}
if (e.key === 'ArrowUp' && cursor !== null && cursor > 0) {
this.setState(prevState => ({
cursor: prevState.cursor - 1,
mouseEnabled: false,
}))
} else if (e.key === 'ArrowDown' && cursor === null) {
this.setState({
cursor: 0,
mouseEnabled: false,
})
} else if (e.key === 'ArrowDown' && cursor < data.length - 1 ) {
this.setState(prevState => ({
cursor: prevState.cursor + 1,
mouseEnabled: false,
}))
} else if (e.key === 'Enter') {
if (this.state.cursor !== null) {
this.loadData(data[cursor].id)
} else {
this.setState({
data: [],
mouseEnabled: true,
})
}
}
}
handleMouseEnter = (id) => {
if (this.state.mouseEnabled !== false) {
this.setState({ cursor: id })
}
}
handleMouseLeave = () => {
if (this.state.mouseEnabled !== false) {
this.setState({ cursor: null })
}
}
handleMouseMove = () => {
this.setState({ mouseEnabled: true })
}
loadData = (id) => {
$.ajax({
url: `${this.props.path}/${id}`,
dataType: 'json',
}).done((data) => {
this.setState({
query: data['name'],
data: [],
mouseEnabled: true,
})
window[this.props.applyDataMethod](data)
this.search.blur()
})
}
render() {
const dropdownIsActive = this.state.query != '' && this.state.data.length > 0
return (
<React.Fragment>
<div className='dropdown-search-wrapper'>
<input
type='text'
placeholder={this.props.inputPlaceholder}
className='form-control'
name={this.props.inputName}
ref={input => this.search = input}
onFocus={this.handleInputChange}
onChange={this.handleInputChange}
value={this.state.query}
autoComplete='off'
onKeyDown={this.handleKeyDown}
/>
{dropdownIsActive &&
<div
ref={clear => this.clear = clear}
className='dropdown-clear-search'
>
&times;
</div>
}
</div>
<div className={`dropdown dropdown-search ${dropdownIsActive ? 'open' : ''}`}>
<div
className='dropdown-menu'
ref={dropdown => this.dropdown = dropdown}
>
<Items
cursor={this.state.cursor}
data={this.state.data}
handleClick={(id) => this.loadData(id)}
handleMouseEnter={(id) => this.handleMouseEnter(id)}
handleMouseLeave={(id) => this.handleMouseLeave(id)}
handleAddCustomer={() => this.addFermentable()}
/>
</div>
</div>
</React.Fragment>
)
}
}
const Items = (props, handleMouseEnter) => {
const items = props.data.map((item, index) => (
<li
key={item.id}
className={index == props.cursor ? 'active' : ''}
onMouseEnter={() => props.handleMouseEnter(index)}
onMouseLeave={props.handleMouseLeave}
>
<button
type='button'
onClick={() => props.handleClick(item.id)}
>
<p className='primary'>
{item.name}
</p>
<p className='secondary'>
{item.type.titleize()} &middot; {item.origin}
</p>
</button>
</li>
))
return (
<ul>
{items}
</ul>
)
}
export default DropdownSearch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment