Documentation

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

SearchResults

Search

 

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

     

    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.