Last active
April 26, 2019 17:15
-
-
Save vmcilwain/a55162f5d7f895478351311937a83a01 to your computer and use it in GitHub Desktop.
React coffescript autocomplete input using datalist, modulejs, underscore, jquery. Not using ES6.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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='#' | |
> | |
× | |
</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