Tutorials: Searching the Catalog
The Component
Now that we have 2 ways to work with the data (via the query or the component), we can attach this to as many components as we want.
In the Sample Web App, we have a Search component that uses 2 smaller components: SearchInput
and SearchResults
.
See the following in this article:
SearchInput
This component can be found at the top middle of the screen.
[screen]
It has default text set to ‘search’. When you focus the search input field, the default text will go away and you can start typing the word you would like to search for. Loading text will appear as you type and the SearchResults
component should appear with some results. We will look into the SearchResults
component in the next section.
[screen]
The SearchInput
also has a clear button on the right. When you click this, the text will be cleared and the results will be hidden.
Let’s see how this was done:
...
const SearchInput = (props) => {
const searchElement = useRef(null);
const resetPlaceholder = () => {
searchElement.current.placeholder = props.placeholder;
};
const onFocus = () => {
const { value } = searchElement.current;
if (!value) {
searchElement.current.placeholder = '';
searchElement.current.value = '';
}
props.onFocus();
};
const onBlur = () => {
const { value } = searchElement.current;
if (!value) {
resetPlaceholder();
}
};
const onClick = () => {
onFocus();
};
const onKeyUp = () => {
const { value } = searchElement.current;
props.onChange(value);
};
const onClear = () => {
searchElement.current.value = '';
props.onChange('');
searchElement.current.focus();
};
return (
);
};
SearchInput.propTypes = {
onFocus: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
className: PropTypes.string.isRequired,
placeholder: PropTypes.string,
initialValue: PropTypes.string,
};
SearchInput.defaultProps = {
placeholder: 'search',
initialValue: '',
};
export default SearchInput;
These properties can be found on the SearchInput
component:
Property | Description | Example |
onFocus |
Function to handle the focus event when focus is on the SearchInput field. | NA |
onChange |
Function to handle the event when text change happens within the component. | NA |
className |
The className to apply to the component for styling purposes. | input-class |
placeholder |
The text to display as the placeholder when no value is in the input field. | Placeholder text |
initialValue |
The initial value added to the input field. | search |
SearchResults
When you enter text in the search input field, the results appear at the bottom. This is done by using the value of the search input field and running the GraphQL Search query with that value. The query returns the results which can be used to display in our component.
[screen]
The search results of the query are displayed in a list with the image of the station on the left and the title next to it. The title is clickable. When you select one of the items it would set the playback source using the source ID of the item. You can read more about setting sources for playback here.
Let’s see how this is done:
...
import { search } from '../../graphql/search/search';
...
const SearchResultItem = props => (
{props.value}
);
SearchResultItem.propTypes = {
imageSrc: PropTypes.string,
linkUrl: PropTypes.string.isRequired,
onClicked: PropTypes.func,
value: PropTypes.string.isRequired,
};
SearchResultItem.defaultProps = {
onClicked: () => {},
};
...
const SearchResults = (props) => {
...
return (
{({ loading, error, data }) => {
if (loading) return Loading...;
if (error) return Error Searching, please try again later.;
if (!data.search || !data.search.items) return No results found.;
if (data.search.items) setSearchResults(data.search.items);
const results = data.search.items.map(({ meta }) => (
));
return (
{results}
);
}}
...
);
};
...
SearchResults.propTypes = {
isVisible: PropTypes.bool,
numberOfResults: PropTypes.number,
onViewAll: PropTypes.func,
onItemClicked: PropTypes.func,
query: PropTypes.string,
};
SearchResults.defaultProps = {
isVisible: false,
numberOfResults: 5,
query: '',
onItemClicked: () => {},
onViewAll: () => {},
};
export { SearchResultItem };
export default withRouter(connect(null, mapDispatchToProps)(SearchResults));
A SearchResultItem
component is also created. This component renders a single result item that contains an image with a clickable title next to it. These properties can be found on the SearchResults
component:
Property | Description | Example |
isVisible |
Whether the search results box should be visible. | false |
numberOfResults |
The number of results to display when searching. This will be passed to the search query as the limit parameter. | 5 |
query |
The text to search for. This will be passed to the search query as the search parameter. | Adele |
onItemClicked |
Function to handle the click event on a search result item. | NA |
onViewAll |
Function to handle the click event on the view all button. | NA |
Search
Finally, we will look at the Search component. This component uses the SearchInput
and SearchResults
components. It can be seen as the orchestrator of these 2 components. It’s main function is to listen on text changes of the SearchInput
component and set the query value of the SearchResults
based on the text entered.
...
import SearchInput from './searchInput';
import SearchResults from './searchResults';
const Search = (props) => {
...
useEffect(() => {
const handleClickOutside = (event) => {
if (searchContainer && !searchContainer.current.contains(event.target)) {
setResultsVisible(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return function cleanup() {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const onSearchInputChanged = (searchText) => {
setSearchQuery(searchText);
if (!searchText) {
setResultsVisible(false);
return;
}
if (!isResultsVisible) {
setResultsVisible(true);
}
};
const debounceSearchInput = debounce(onSearchInputChanged, props.debounceTime);
const onSearchInputFocus = () => {
if (searchQuery) {
setResultsVisible(true);
}
};
const closeResults = () => {
setResultsVisible(false);
};
return (
);
};
Search.propTypes = {
className: PropTypes.string,
debounceTime: PropTypes.number,
inputPlaceholder: PropTypes.string,
numberOfSearchResults: PropTypes.number,
};
Search.defaultProps = {
numberOfSearchResults: 5,
inputPlaceholder: 'search',
debounceTime: 0,
};
const mapStateToProps = state => (
{
searchQuery: state.search.query,
}
);
export default connect(mapStateToProps)(Search);
The handleClickOutside
is used to react on clicks outside of the Search component area and close the search results once a click is registered. It does so by setting the isVisible
property on the SearchResults
component.
Debounce
is used to improve performance. When the user enters text in the SearchInput
component, it is sent to the SearchResults
component that will run the query with that value and return results. If this is done on each keystroke you end up seeing a delay in the rendering of the component. Debounce
will then wait a specified amount of time to set the query property on SearchResults
, resulting in a smoother UI.