Last active
January 25, 2016 00:22
-
-
Save jmccartie/2c76022b8cade2e0aa27 to your computer and use it in GitHub Desktop.
First shot at some React
This file contains 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
(function($){ | |
$.fn.searchField = function(){ | |
this.each(function(_index, entry){ | |
var $entry = $(entry); | |
//1. Add the results dropdown page | |
// - add the pane | |
// - add the help message | |
// - add the results list | |
//2. Handle the dropdown events | |
// - show and hide the dropdown | |
//3. Handle the visibility of the contents of the dropdown | |
//4. Populate the results list upon text entry | |
//5. Handle the down and up arrows | |
//6. Handle the item navigation upon selection | |
var data = { | |
oldEntryText: "", | |
entryText: "", | |
results: [], | |
grouped: {campgrounds: [], places: [], cities: [], states: []}, | |
hasFocus: false, | |
selectedIndex: -1 | |
}; | |
var dropdownTemplate = "<div class='search-field-dropdown'><div class='search-field-dropdown-help'></div><div class='search-field-dropdown-results'></div></div>"; | |
var helpTemplate = "<div class='search-field-help'>Start typing...<br /> - City, State or Zip Code<br /> - National Park, National Forest, State Park<br /> - Campground Name</div>"; | |
var resultsTemplate = "<div class='search-field-results'></div>"; | |
// DOM manipulation part | |
$entry.attr('autocomplete', 'off'); | |
$entry.after(dropdownTemplate); | |
var $dropdown = $entry.next(); | |
$dropdown.append(helpTemplate).append(resultsTemplate); | |
var $help = $dropdown.find('.search-field-help'); | |
var $results = $dropdown.find('.search-field-results'); | |
var groupResults = function(results){ | |
var pregrouped = _.groupBy(results, 'type'); | |
var grouped = {}; | |
grouped.campgrounds = pregrouped['Campground']; | |
grouped.places = _.sortBy(pregrouped['Place'], "title"); | |
grouped.cities = _.sortBy(pregrouped['City'], "title"); | |
grouped.states = _.sortBy(pregrouped['State'], "title"); | |
return grouped; | |
}; | |
var updateResults = _.debounce(function(){ | |
$.getJSON('/search.json', { phrase: data.entryText }, function(reply){ | |
data.results = reply; | |
data.grouped = groupResults(data.results); | |
data.selectedIndex = -1; | |
synchronize(false); | |
}); | |
}, 300); | |
var updateData = function(fromEvent){ | |
if(fromEvent){ | |
data.oldEntryText = data.entryText; | |
data.entryText = $entry.val(); | |
if(data.oldEntryText !== data.entryText){ | |
updateResults(); | |
} | |
} | |
}; | |
var handleItemSelect = function(e){ | |
var $el = $(e.currentTarget); | |
window.location.href = $el.data('url'); | |
return false; | |
}; | |
var updateContentsVisibility = function(){ | |
var funcs = data.results.length > 0 ? ['hide', 'show'] : ['show', 'hide']; | |
$help[funcs[0]](); | |
$results[funcs[1]](); | |
var dropdownFunc = data.hasFocus ? 'show' : 'hide'; | |
$dropdown[dropdownFunc](); | |
}; | |
var updateResultsList = function(){ | |
var entities = [['campgrounds', 'Campgrounds'], ['places', 'Places'], ['cities', 'Cities'], ['states', 'States']].reverse(); | |
$results.html(_.map(entities, function(o){ | |
return "<div class='results-" + o[0] + "'><h3>" + o[1] + "</h3><ul></ul></div>"; | |
}).join("\n")); | |
_.each(entities, function(e){ | |
var $list = $results.find(".results-" + e[0] + " ul"); | |
if(data.grouped[e[0]] !== undefined && data.grouped[e[0]].length > 0){ | |
_.each(data.grouped[e[0]], function(result){ | |
$list.append("<li data-url='" + result.url + "'>" + result.title + "</li>"); | |
}); | |
$('li', $list).on('click', handleItemSelect); | |
} | |
else { | |
$list.parent().hide(); | |
} | |
}); | |
}; | |
var synchronize = function(fromEvent){ | |
updateData(fromEvent); | |
updateContentsVisibility(); | |
if(fromEvent == false){ | |
updateResultsList(); | |
} | |
}; | |
var updateSelection = function(delta){ | |
data.selectedIndex = data.selectedIndex + delta; | |
if(data.selectedIndex < 0) data.selectedIndex = 0; | |
if(data.selectedIndex >= data.results.length) data.selectedIndex = data.results.length - 1; | |
$results.find('li').removeClass('hovered'); | |
$($results.find('li')[data.selectedIndex]).addClass('hovered'); | |
}; | |
var navigateSelected = function(){ | |
if(data.selectedIndex !== -1){ | |
var url = $($results.find('li')[data.selectedIndex]).data('url'); | |
window.location.href = url; | |
} | |
else { | |
$entry.parents('form').first().submit(); | |
} | |
return false; | |
}; | |
// Basic spatial positioning part | |
var updateSpatialPositioning = function(){ | |
var entryBottom = $entry.position().top + $entry.outerHeight(true); | |
$dropdown.css('width', $entry.outerWidth(true)); | |
$dropdown.css('top', entryBottom + 2); | |
} | |
var initialize = function(){ | |
$entry.on('keyup', function(){ | |
synchronize(true); | |
}); | |
$entry.on('focusin', function(){ | |
data.hasFocus = true; | |
synchronize(true); | |
}); | |
$entry.on('keydown', function(e){ | |
switch(e.which){ | |
case 38: // up | |
updateSelection(-1); | |
e.preventDefault(); | |
break; | |
case 40: // down | |
updateSelection(1); | |
e.preventDefault(); | |
break; | |
case 13: // enter | |
navigateSelected(); | |
e.preventDefault(); | |
break; | |
default: return; | |
} | |
}); | |
$(window).on('click', function(e){ | |
if(e.target !== $entry[0] && $(e.target).parents('.search-field-dropdown').length == 0 ){ | |
data.hasFocus = false; | |
synchronize(true); | |
} | |
}); | |
$(window).on('resize', updateSpatialPositioning); | |
updateSpatialPositioning(); | |
synchronize(false); | |
return $entry; | |
}; | |
return initialize(); | |
}); | |
return this; | |
}; | |
$(document).ready(function(){ | |
$('.search-field').searchField(); | |
}); | |
}(jQuery)); |
This file contains 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
var SearchField = React.createClass({ | |
getInitialState: function() { | |
return {dropdownVisibility: 'none', helpVisibility: 'block', results: []}; | |
}, | |
fetchResults: function(event) { | |
$.getJSON('/search.json', { q: event.target.value }, function(data){ | |
this.setState({ results: data }); | |
if (data.length > 0) { | |
this.hideHelp(); | |
} else { | |
this.showHelp(); | |
} | |
}.bind(this)); | |
}, | |
showHelp: function() { | |
this.setState({ helpVisibility: 'block'}); | |
}, | |
hideHelp: function() { | |
this.setState({ helpVisibility: 'none'}); | |
}, | |
showDropdown: function() { | |
this.setState({dropdownVisibility: 'block'}); | |
}, | |
hideDropdown: function () { | |
this.setState({dropdownVisibility: 'none'}); | |
}, | |
render: function() { | |
return <div> | |
<input id="q_ftx_search_cont" className="string required form-control search-field" | |
autoComplete="off" name="q[ftx_search_cont]" placeholder="Search" type="Text" | |
onKeyUp={this.fetchResults} onFocus={this.showDropdown} /> | |
<div className='search-field-dropdown' style={{display: this.state.dropdownVisibility}}> | |
<div className='search-field-dropdown-help'></div> | |
<div className='search-field-dropdown-results'> | |
{this.state.results.map(function(result) { | |
return <SearchFieldGroup key={result.title} data={result}/>; | |
})} | |
</div> | |
<div className='search-field-help' style={{display: this.state.helpVisibility}}>Start typing...<br /> - City, State or Zip Code<br /> - National Park, National Forest, State Park<br /> - Campground Name</div> | |
</div> | |
</div> | |
; | |
} | |
}); |
This file contains 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
var SearchFieldGroup = React.createClass({ | |
render: function() { | |
return <div> | |
<h3>{this.props.data.title}</h3> | |
<ul> | |
{this.props.data.results.map(function(result) { | |
return <SearchFieldResult key={result.id} data={result} /> | |
})} | |
</ul> | |
</div> | |
; | |
} | |
}); |
This file contains 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
var SearchFieldResult = React.createClass({ | |
visitPath: function() { | |
window.location = this.props.data.url; | |
}, | |
render: function() { | |
return <li><span onClick={this.visitPath}>{this.props.data.title}</span></li>; | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This looks like the right path to me! My thoughts on your specific questions:
I think your
state
is reaching too deep into how to handle "help being visible". Instead of relying on toggling styles, If "help isn't visible", you can omit the node completely with something likeAre you talking about the
key
prop? If so, that is a bit of boilerplate that we have to live with. As far as I know, it helps the DOM diffing, == performanceI've built similar components, and I handle it in the topmost component. Assuming that is where the user's focus still is, that's the appropriate place to handle the event.
What was your motivation for:
Is there a reason you want to avoid the anchor tag with?