I am working on integrating react-bootstrap-typeahead into my application as the basis for a search box with async autocomplete suggestions. After a few hours of playing and familiarizing myself with the library, I have the search functionality 99% correct minus 1 use case.
Ironically, the last use case I need to get working is when the user doesn't bother to interact with the typeahead suggestions and just enters a query and presses the ENTER Key. The result of this should be that the application performs a search with the value that has been manually entered into the textbox. When the user hits enter though, nothing happens because the typeahead does not act unless the variable Typeahead variable activeItem is truthy and it can select the active item. I realize that this is expected behavior, but after a few hours of toying with methods, I have been unable to find a solution that allows me to get the behavior that I am looking for.
My first attempt at this was to use the obvious onKeyDown
property and supply the following handler:
onKeyDown={(event) => event.which === 13 && executeSearch(searchQuery)} // searchQuery state kept in sync with inputs and selections
This attempt allowed me to get the desired functionality for this use case, unfortunately I later determined that it broke the default activeItem selection functionality. If the user has used the UP and DOWN keys to select a menu item, I would end up with two search requests because the onKeyDown handler submitted an erroneous request with the current value in the input and an expected request with the from the onChange handler. I seem to have hit a wall here as I cannot access the activeItem variable to determine if an onChange call will be coming down the pipe. Is there an alternative way of handling this that I am missing?
For completions sake, here is my entire component:
const suggesterType = {
autocomplete: "Autocomplete",
suggestions: "Suggestions"
};
const currentSuggester = suggesterType.autocomplete;
export default ({ search }) => {
const { state: { searchText: initialSearch = '' } = {} } = useLocation();
const [searchQuery, setSearchQuery] = useState(initialSearch);
const [isLoading, setIsLoading] = useState(false);
const [options, setOptions] = useState([]);
const getSuggestions = useCallback((searchTerm) => {
setIsLoading(true);
if (currentSuggester === suggesterType.autocomplete) {
const autocompletePrefix = searchTerm.replace(/[\d\w]+\s?$/, '');
productsApi.autocomplete({ searchTerm }).then(results => {
setOptions(results.map(term => autocompletePrefix + term));
setIsLoading(false);
});
}
else {
productsApi.suggestProducts({ searchTerm }).then(results => {
setOptions(results)
setIsLoading(false);
});
}
}, []);// eslint-disable-line react-hooks/exhaustive-deps
const handleSearch = (e) => {
console.log("form submitted:", searchQuery);
executeSearch(searchQuery);
e.preventDefault();
}
const executeSearch = (query) => {console.log('searching', query); search && search(query);}
return (
<Form onSubmit={handleSearch}>
<InputGroup className="search-bar">
<AsyncTypeahead
id="searchbox"
placeholder="Search"
autoFocus={initialSearch?.length === 0}
isLoading={isLoading}
minLength={3}
defaultInputValue={searchQuery}
onKeyDown={(event) => { console.log('onKeyDown', searchQuery, event.defaultPrevented); event.which === 13 && executeSearch(searchQuery)}}
onInputChange={(query) => { console.log('oninputchange', query); setSearchQuery(query); }}
onChange={([query]) => { console.log('onchange', query); executeSearch(query); }}
onSearch={getSuggestions}
options={options}
/>
<InputGroupAddon addonType="append">
<InputGroupText>
<Button color="link" type="submit">
<FontAwesomeIcon icon={faSearch}></FontAwesomeIcon>
</Button>
</InputGroupText>
</InputGroupAddon>
</InputGroup>
</Form>
);
}
Submission of arbitrary values isn't a first-class use case for the library, so it's not well-supported but it is possible. Here's one workaround:
// Track the index of the highlighted menu item.
const [activeIndex, setActiveIndex] = useState(-1);
const onKeyDown = useCallback(
(e) => {
// Check whether the 'enter' key was pressed, and also make sure that
// no menu items are highlighted.
if (e.keyCode === 13 && activeIndex === -1) {
// Execute the search.
}
},
[activeIndex]
);
return (
<AsyncTypeahead
...
onKeyDown={onKeyDown}>
{(state) => {
// Passing a child render function to the component exposes partial
// internal state, including the index of the highlighted menu item.
setActiveIndex(state.activeIndex);
}}
</AsyncTypeahead>
);