Skip to content

Instantly share code, notes, and snippets.

@vmcilwain
Last active April 26, 2019 17:15
Show Gist options
  • Save vmcilwain/a55162f5d7f895478351311937a83a01 to your computer and use it in GitHub Desktop.
Save vmcilwain/a55162f5d7f895478351311937a83a01 to your computer and use it in GitHub Desktop.
React coffescript autocomplete input using datalist, modulejs, underscore, jquery. Not using ES6.
# Shouldn't be difficult to convert to ES6
# JQuery is used for the API call ONLY. I did this because I had to. DON'T DO THIS!
# Note: additionalInfoOptions is used to store additional data about the record. If data is
# coming and going from the same interface its should not be needed. This was not case of writing this code.
modulejs.define 'slzr/react/autocomplete_input',
['react', 'prop-types', 'underscore', 'jquery'],
(React, PropTypes, _, $) ->
class AutocompleteInput extends React.Component
@propTypes:
inputType: PropTypes.string
inputName: PropTypes.string
placeHolder: PropTypes.string
dataListID: PropTypes.string
selectListID: PropTypes.string
searchKeys: PropTypes.array
optionAdded: PropTypes.func
optionRemoved: PropTypes.func
url: PropTypes.string
selected: PropTypes.array
additionalInfo: PropTypes.array
additionalInfoOptions: PropTypes.object
optionAddedOptions: PropTypes.array
optionFormatterOptions: PropTypes.object
loadingMessage: React.PropTypes.string
@defaultProps:
inputType: 'text'
inputName: 'generic-input'
placeHolder: 'Start typing'
dataListID: 'generic-data-list'
selectListID: 'generic-selected-list'
searchKeys: ['name', 'email','username']
optionAddedOptions: ['id', 'name', 'email']
optionFormatterOptions: {key: 'id', value: 'name', display: 'name'}
additionalInfoOptions: {match: 'id', key: 'id', data_id: 'id', display: 'name'}
loadingMessage: 'Loading...'
constructor: (props) ->
super(props)
@state =
searchText: ""
data: []
# Format records returned from search into option elements for datalist
# @params record [Object], a record object
# @params options [Object], specified attributes of the object to build the option element
# @returns JSX option <object>
# see optionFormatterOptions default props
optionFormatter: (record, options) =>
return `<option key={record[options.key]}
value={record[options.value]}
>
{record[options.display]}
</option>`
# Format records as unordered list elements for displaying the selecteded list
# @params id [Number], the record id
# @params options [Object], the specified attributes of the object to build the list element (based on additionalInfo prop)
# returns JSX <li>
# see additionalInfo default props
listFormatter: (id, options) =>
my = this
info = _.find this.props.additionalInfo, (i) -> i[options.match] == id
if info != undefined # ignore while searching for a record
display = info[options.display]
return `<li key={info[options.key]}
className="picked_item"
>
{display}
<a
data-id={info[options.data_id]}
onClick={my.deleteSelected}
href='#'
>
&times;
</a>
</li>`
# Filter results of partial matches to full matches
# @params searchText [String], the text in the input from the onChange func.
# @returns [Array], an array of matching results as objects.
# see searchKeys prop
# see data state
fullMatch: (searchText) =>
my = this
_.find this.state.data, (record) ->
for search_key in my.props.searchKeys
if record[search_key] == searchText
return record
break
# Filter results from search to partial matches
# @returns [Array], the list of partial matches as objects.
# see searchKeys prop
filterResults: () =>
regx = new RegExp(this.state.searchText, 'gi')
my = this
_.filter this.state.data, (record) ->
for search_key in my.props.searchKeys
if record[search_key].match(regx)
return record
break
# Get data based on search text
# @params searchText [String], the text used for the pull of records
# @returns data [array], array of record as objects
getData: (searchText) =>
$('#status-message').html(this.props.loadingMessage)
my = this
$.ajax
async: true,
type: "GET",
global: false,
dataType: 'JSON',
url: this.props.url,
data: { 'value': searchText },
success: (data) ->
$('#status-message').html('')
my.setState
data: data
# Search and narrow options down
# @params e [Event], the input event
# see getData func
# see fullMatch func
# see addSelected func
onChangeHandler: (e) =>
e.preventDefault()
prevSearchText = this.state.searchText
searchText = e.target.value
this.setState
searchText: searchText
# Clear data list when user clears the input field.
if searchText == ""
this.setState
searchText: ""
data: []
return
# Don't run any code until the user types at leaast 2 characters
if searchText.length >= 2
starterRegx = new RegExp('^' + searchText.substring(0,2), 'i')
# Get data in the event the first 2 characters do not match otherwise continue to update the searchText
if this.state.searchText.match(starterRegx)
this.setState
searchText: searchText
else
this.getData(searchText)
# Check for full match
fullMatch = this.fullMatch(searchText)
# Add record if full match exists
if typeof fullMatch == 'object'
this.addSelected(fullMatch)
# Add record to component (redux).
# @params fullMatch [String], the record object
# see optionAddedOptions prop
# see optionAdded prop
addSelected: (fullMatch) =>
if this.props.optionAddedOptions.length > 0
options = {}
for key in this.props.optionAddedOptions
options[key] = fullMatch[key]
this.setState
searchText: ''
data: []
# redux action
# Delete record from component (redux)
# @params e [Event], the click event
# see optionRemoved prop
deleteSelected: (e) =>
e.preventDefault()
id = e.target.dataset['id']
#redux action
render: ->
# Find partial matches
results = this.filterResults()
my = this
# Convert results to array of JSX <option></option>
formattedResults = _.map results, (record) -> my.optionFormatter(record, my.props.optionFormatterOptions)
# Convert records in redux to to JSX <li></li> for displaying
selectedItems = _.map this.props.selected, (id) -> my.listFormatter(id, my.props.additionalInfoOptions)
`<div>
<input
type={this.props.inputType}
name={this.props.inputName}
placeholder={this.props.placeHolder}
list={this.props.dataListID}
onChange={this.onChangeHandler}
value={this.state.searchText}
/>
<span id='status-message'></span>
<datalist id={this.props.dataListID}>
{formattedResults}
</datalist>
<div className='filter-selector'>
<ul id={this.props.selectListID} className="picked_item_list">
{selectedItems}
</ul>
</div>
</div>`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment