HomeDev guideRecipesAPI ReferenceGraphQL
Dev guideUser GuideGitHubNuGetDev CommunitySubmit a ticketLog In
GitHubNuGetDev CommunitySubmit a ticket

Apply autocomplete to the search page

This tutorial applies autocomplete to the search page for the Music Festival site.

Preparation

Tutorial

  1. Go to /decoupled-site/react-script/src/graphql.

  2. Add a new file ArtistAutocomplete.graphql, and add the following code.

    query ArtistAutocomplete($searchParam: String!) {
      ArtistDetailsPage {
        autocomplete {
          StageName(value: $searchParam)
          ArtistName(value: $searchParam, limit: 3)
        }
      }
    }
    
  3. Go to the components folder, open SearchButton.tsx and update the file as shown in the following code block.

    import { Autocomplete, TextField } from "@mui/material";
    import { useState } from "react";
    import { useSearchParams } from "react-router-dom";
    import { ArtistAutocompleteQuery, useArtistAutocompleteQuery } from "../generated";
    import { generateGQLSearchQueryVars } from "../helpers/queryCacheHelper";
    import { isEditOrPreviewMode } from "../helpers/urlHelper";
    const singleKeyUrl = process.env.REACT_APP_CONTENT_GRAPH_GATEWAY_URL as string
    
    type CustomString = string | number | readonly string\[] | undefined
    
    function SearchButton(): JSX.Element {  
      const [searchParams] = useSearchParams()  
      const [token, setToken] = useState("")  
      const [isShown, setIsShown] = useState(false)  
      const [searchValue, setSearchValue] = useState<CustomString>(searchParams.get("q")?.toString())  
      const [orderBy, setOrderBy] = useState("ASC")  
      let variables: any = generateGQLSearchQueryVars(token, window.location.pathname, searchValue as string | null, orderBy);  
      const modeEdit = isEditOrPreviewMode()  
      let stringArr: (string | null)\[] = \[]
      let autocompleteData : ArtistAutocompleteQuery | undefined = undefined
      const { data : artistAutocompleteData } = useArtistAutocompleteQuery({ endpoint: singleKeyUrl }, variables, { staleTime: 2000, enabled: !modeEdit || !!token })
      autocompleteData = artistAutocompleteData
    
    function search(event: any, action: string){
      if ((action == "keypress" && event.charCode === 13) || action == "buttonclick") {
        window.location.href = `${window.location.origin}/search?q=${searchValue}`
      }
    }
    
    function onValueChange(event: any){
      setSearchValue(event.target.value);
      event.target.value != "" && event.target.value !== undefined ? setIsShown(true) : setIsShown(false);
    }
    
    function onAutoClick(event: any){
      setSearchValue(event.target.textContent);
      window.location.href = `${window.location.origin}/search?q=${event.target.textContent}`
    }
    
    return (
      <div>
        <div className="nav-table-cell">
          <input className="search-input" type="text" id="search-input" placeholder="Search" 
                 onKeyPress={(event) => {search(event, 'keypress')}} value={searchValue} onChange={onValueChange} />
          <a className="search-icon" onClick={(event) => {search(event, 'buttonclick')}}>
            <i className="fa fa-search"></i>
          </a>
          <div className="autocomplete-block" style={isShown ? {display: "inherit"}: {display: "none"}}>
            {
              autocompleteData?.ArtistDetailsPage?.autocomplete?.ArtistName?.map((name) => {
                return(
                  <div key={name} onClick={(event) => onAutoClick(event)}>{name}</div>
                )
              })                    
            }
            {
              autocompleteData?.ArtistDetailsPage?.autocomplete?.StageName?.map((name) => {
                return(
                  <div key={name} onClick={(event) => onAutoClick(event)}>{name}</div>
                )
              })
            }
          </div>
        </div>
      </div>
      );
    }
    
    export default SearchButton;
    
  4. Go to the SearchPage.tsx and update the file with the following code.

    import { useState } from "react";
    import { useSearchParams } from "react-router-dom";
    import Footer from "../components/Footer";
    import Header from "../components/Header";
    import SearchButton from "../components/SearchButton";
    import { ArtistAutocompleteQuery, ArtistSearchQuery, OtherContentSearchQuery, useArtistAutocompleteQuery, useArtistSearchQuery, useOtherContentSearchQuery } from "../generated";
    import { generateGQLSearchQueryVars } from "../helpers/queryCacheHelper";
    import { getImageUrl, isEditOrPreviewMode } from "../helpers/urlHelper";
    import ReactPaginate from 'react-paginate';
    
    const singleKeyUrl = process.env.REACT_APP_CONTENT_GRAPH_GATEWAY_URL as string
    
    function SearchPage() {
      const [token, setToken] = useState("")
      const [itemOffset, setItemOffset] = useState(0)
      const [otherItemOffset, setOtherItemOffset] = useState(0)
      const [itemsPerPage, setItemsPerPage] = useState(10)
      const [otherItemsPerPage, setOtherItemsPerPage] = useState(10)
      const [filterBy, setFilterBy] = useState("Artist")
      const [orderBy, setOrderBy] = useState("ASC")
      const [searchParams] = useSearchParams()
      const endOffset = itemOffset + itemsPerPage;
      const endOffsetOther = otherItemOffset + otherItemsPerPage;
      const modeEdit = isEditOrPreviewMode()
      let data: ArtistSearchQuery | undefined = undefined
      let otherData: OtherContentSearchQuery | undefined = undefined
      let autocompleteData : ArtistAutocompleteQuery | undefined = undefined
      let queryString: string | null
      let resultNumber : number
      let otherResultNumber : number
      let variables: any
      let options: {value: string; key: string}[] = [
        {value: "ASC", key: "ASC"},
        {value: "DESC", key: "DESC"}
      ]
      let itemsPerPageOptions: {value: number; key: string}[] = [
        {value: 10, key: "10"},
        {value: 15, key: "15"}
      ]
      let filterByOption: {value: string; key: string}[] = [
        {value: "Artists", key: "Artist"},
        {value: "Other Content", key: "OtherContent"}
      ]
    
       queryString = searchParams.get("q")
       if(queryString === undefined || queryString == 'undefined'){
         queryString = ""
     }
    
     variables = generateGQLSearchQueryVars(token, window.location.pathname, queryString, orderBy);
    
     const { data : searchQueryData } = useArtistSearchQuery({ endpoint: singleKeyUrl }, variables, { staleTime: 2000, enabled: !modeEdit || !!token });
     data = searchQueryData
     resultNumber = data?.ArtistDetailsPage?.items?.length ?? 0
     const currentItems = data?.ArtistDetailsPage?.items?.slice(itemOffset, endOffset);
     const pageCount = Math.ceil(resultNumber / itemsPerPage);
    
     const { data : otherContentSearchQueryData } = useOtherContentSearchQuery({ endpoint: singleKeyUrl }, variables, { staleTime: 2000, enabled: !modeEdit || !!token });
     otherData = otherContentSearchQueryData
     otherResultNumber = otherData?.Content?.items?.length ?? 0
     const currentOtherItems = otherData?.Content?.items?.slice(otherItemOffset, endOffsetOther);
     const pageOtherCount = Math.ceil(otherResultNumber / itemsPerPage);
    
     const { data : artistAutocompleteData } = useArtistAutocompleteQuery({ endpoint: singleKeyUrl }, variables, { staleTime: 2000, enabled: !modeEdit || !!token })
     autocompleteData = artistAutocompleteData
    
     const handlePageClick = (event: any) => {
       const newOffset = (event.selected * itemsPerPage) % resultNumber;
       setItemOffset(newOffset);
     };
    
     const handleItemsChange = (event: any) => {
       setItemsPerPage(event.target.value);
     };
    
     const handleOtherPageClick = (event: any) => {
       const newOffset = (event.selected * itemsPerPage) % otherResultNumber;
       setOtherItemOffset(newOffset);
     };
    
     const handleOtherItemsChange = (event: any) => {
       setOtherItemsPerPage(event.target.value);
     };
    
     const handleFilterByChange = (event: any) => {
       setFilterBy(event.target.value);
     };
    
     const handleChange = (event: any) => {
       setOrderBy(event.target.value);
     }
    
     const handleFacetClick = (event: any) => {
       window.location.href = `${window.location.origin}/search?q=${event.target.innerText}`
     }
    
     return (
       <div>
         <Header />
         <div className="search-container">
           <div className="back-button">
             <a href={window.location.origin} className="home-link">
               <span>Back to Landing page</span>
             </a>
           </div>
           <div className="search-zone">
             <div style={{float: "left"}}>
               <SearchButton />
             </div>
             <div style={{float: "right"}}>
               <span>Search by: </span>
               <select className="Button" onChange={handleFilterByChange}>
                 {
                   filterByOption.map((option) => {
                     return (
                       <option key={option.key} value={option.key}>{option.value}</option>
                     )
                   })
                 }
               </select>
             </div>
           </div>
           <div className="search-panel">
             <div className="left-panel">
              <b>Filter by: </b>
              <div className="facets" style={filterBy == "Artist" ? {display: "inherit"}: {display: "none"}}>
                <b>Artist Name: </b>
                  {
                    data?.ArtistDetailsPage?.facets?.ArtistName?.map((artist) => {
                      return (
                        <div>
                          <a key={artist?.name} onClick={(event) => handleFacetClick(event)}>
                            <span>{artist?.name}</span>
                            &nbsp;
                            <b>({artist?.count})</b>
                          </a>
                        </div>
                      )
                    })
                  }
              </div>
              <div className="facets" style={filterBy == "Artist" ? {display: "inherit"}: {display: "none"}}>
                <b>Stage Name: </b>
                {
                  data?.ArtistDetailsPage?.facets?.StageName?.map((artist) => {
                    return (
                      <div>
                        <a key={artist?.name} onClick={(event) => handleFacetClick(event)}>
                          <span>{artist?.name}</span>
                          &nbsp;
                          <b>({artist?.count})</b>
                        </a>
                      </div>
                    )
                  })
                }
              </div>
              <div className="facets" style={filterBy == "OtherContent" ? {display: "inherit"}: {display: "none"}}>
                <b>Content: </b>
                {
                  otherData?.Content?.facets?.Name?.map((content) => {
                    return (
                      <div>
                        <a key={content?.name} onClick={(event) => handleFacetClick(event)}>
                          <span>{content?.name}</span>
                          &nbsp;
                          <b>({content?.count})</b>
                        </a>
                      </div>
                    )
                  })
                }
              </div>
            </div>
            <div className="right-panel">
              <div className="search-description">
                <h6>Your search for <span className="search-term">{queryString}</span> resulted in <span className="search-term">{filterBy == "Artist" ? resultNumber : otherResultNumber}</span> hits</h6>                            
              </div>
              <div className="search-sorting">
                <span>Sort: </span>
                <select onChange={e => handleChange(e)} className="Button">
                  {
                    options.map((option) => {
                      return (
                        <option key={option.key} value={option.key}>{option.value}</option>
                      )
                    })
                  }
                </select>
              </div>
              <div className="result-block">
                <div style={filterBy == "Artist" ? {display: "initial"}: {display: "none"}}>
                  <div className="search-results">
                    {
                      currentItems?.map((content, idx) => {
                        return (
                          <div className="result" key={idx}>
                            <div className="card">
                              <div className="round">
                                <img className="ConditionalImage"
                                  src={getImageUrl(content?.ArtistPhoto ?? '')}
                                  alt={content?.ArtistName ?? ''} />
                              </div>
                              <div className="info">
                                <a href={content?.RelativePath ?? ''} className="EPiLink">
                                  <p className="result-name">{content?.ArtistName}</p>
                                </a>
                              </div>
                            </div>                             
                            <div>
                              <p className="result-description">{content?.ArtistDescription}</p>
                            </div>
                          </div>
                        )
                      })
                    }
                  </div>
                  <div className="search-description">
                    <h6>People also search for: </h6>
                    <br></br>
                    {
                      autocompleteData?.ArtistDetailsPage?.autocomplete?.ArtistName?.map((name) => {
                        return (
                          <div>
                            <a key={name} onClick={(event) => handleFacetClick(event)}>                                                    
                              <i>{name}</i>
                            </a>
                          </div>
                        )
                      })
                    }
                    {
                      autocompleteData?.ArtistDetailsPage?.autocomplete?.StageName?.map((name) => {
                        return (
                          <div>
                            <a key={name} onClick={(event) => handleFacetClick(event)}>                                                    
                              <i>{name}</i>
                            </a>
                          </div>
                        )
                      })
                    }
                  </div>
                  <div className="search-pagination-block">
                    <table>
                      <tbody>
                        <tr>
                          <td>
                            <span>Items per page: </span>
                            <select className="Button" onChange={handleItemsChange}>
                              {
                                itemsPerPageOptions.map((option) => {
                                  return (
                                    <option key={option.key} value={option.key}>{option.value}</option>
                                  )
                                })
                              }
                             </select>
                          </td>
                          <td className="search-pagination">
                            <ReactPaginate
                              breakLabel="..."
                              nextLabel=">"
                              onPageChange={handlePageClick}
                              pageRangeDisplayed={5}
                              pageCount={pageCount}
                              previousLabel="<"
                              renderOnZeroPageCount={null}
                            />
                          </td>
                        </tr>
                      </tbody>
                    </table>
                  </div>
                </div>
                <div style={filterBy == "OtherContent" ? {display: "initial"}: {display: "none"}}>
                  <div className="search-results">
                    {
                      currentOtherItems?.map((content, idx) => {
                        return (
                          <div className="result" key={idx}>
                            <div className="card">
                              <i className="fa fa-file"></i>
                              &nbsp;
                              <div className="info">
                                <a href={content?.RelativePath ?? ''} className="EPiLink">
                                  <p className="result-name">{content?.Name}</p>
                                </a>
                              </div>
                            </div>                             
                            <div>
                              <p className="result-description">{content?.RelativePath}</p>
                            </div>
                          </div>
                        )
                      })
                    }
                  </div>
                  <div className="search-pagination-block">
                    <table>
                      <tbody>
                        <tr>
                          <td>
                            <span>Items per page: </span>
                            <select className="Button" onChange={handleOtherItemsChange}>
                              {
                                itemsPerPageOptions.map((option) => {
                                  return (
                                    <option key={option.key} value={option.key}>{option.value}</option>
                                  )
                                })
                              }
                            </select>
                          </td>
                          <td className="search-pagination">
                            <ReactPaginate
                              breakLabel="..."
                              nextLabel=">"
                              onPageChange={handleOtherPageClick}
                              pageRangeDisplayed={5}
                              pageCount={pageOtherCount}
                              previousLabel="<"
                              renderOnZeroPageCount={null}
                            />
                          </td>
                        </tr>
                      </tbody>
                    </table>
                  </div>
                </div>
              </div>
            </div>
          </div>
          <Footer content={null}/>
        </div>
      </div>
      );
    }
    
    export default SearchPage;
    
  5. Start the site. From now on, whenever you search for a phrase, the system automatically displays results that match with your search phrase. Also, at the end of the search page, the People also search for section displays results that match your search term.