Jump to content

Ajax-driven autocomplete search with native JavaScript


Stefanowitsch
 Share

Recommended Posts

In this tutorial I want to show you how to set up a simple ajax-driven auto-complete search within ProcessWire.

Something that looks like this:

image.gif.eca8c40f28098bac68fe1c7dfce7c050.gif

Requirements:

1. Use the Fieldtype Cache to create a search index

For fast and easy search queries we will create a search index field. You can read more on this here: https://processwire.recipes/recipes/set-up-search-index-with-fieldtypecache/

  1. First of all, go to Modules and install a Core module called FieldtypeCache.
  2. Add a new field “search_cache” and select “Cache” as it’s type. Save the field.
  3. On the Details tab choose the fields you want to make searchable. Save again.
  4. Add “search_cache” to all templates you want to include in your search results.
  5. Optional but recommended: use the “Regenerate Cache” option found from the Details tab of the field to make existing content instantly searchable.

In my case i only want to search inside body and title fields, so I included those fields in the settings of the search cache field:

image.png.b22ea1df8bed315409b6174151842c9e.png

2. Install the Pages2JSON module

We want to make ajax requests to the search template and those results should be returned in JSON format so that our java script can process the results inside the auto-complete dropdown. Therefore we make use of this module here:

https://processwire.com/modules/pages2-json/

In the module settings we define what data will be included in the JSON object that is returned. Remember that this is the data that we want to display in our auto-complete dropdown. So for my case I only want the title and the url field.

image.png.f8e48a2ce1287860784e5b1195321542.png

 

Now let's start:

1. Setting up the search template

Set up a template file called „search“. Then create a page using this template. On most ProcessWire installations this is already the case and this template exits in the file system. This template will handle the search queries and list all the results for us and that in two ways:

- you can send a search query and the search result page will list all the results (as you would expect)

- you can send an ajax search query and the result page will return the results as a json object to be processed within javascript and displayed in the frontend in real-time

search.php:

<?php
if($config->ajax) {
    // Return search results in JSON format
    $q = $sanitizer->selectorValue($input->get->q);
    $results = $pages->find('search_cache%=' . $q);; // Find all pages and save as $results
    header("Content-type: application/json"); // Set header to JSON
    echo $results->toJSON(); // Output the results as JSON via the toJSON function
    return $this->halt();
}
?>
<main>
  <div class="uk-container">
    <?php

    // look for a GET variable named 'q' and sanitize it
    $q = $sanitizer->selectorValue($input->get->q);

    // did $q have anything in it?
    if($q) {

      // Find pages that match the selector
      $matches = $pages->find('search_cache%=' . $q);

      // did we find any matches? ...
      if($matches->count) {

        echo "<h2>We found $matches->count results:</h2>";

        echo "<ul class='uk-list uk-list-square'>";

        foreach($matches as $match) {
          echo "<li><a href='$match->url'>$match->title</a>";
          echo "<div class='summary'>$match->summary</div></li>";
        }

        echo "</ul>";

      } else { ?>
    <h2>No results found.</h2>
    <div uk-grid class="uk-flex uk-flex-center">
      <div class="uk-width-1-1 uk-width-1-2@m">
        <?= $files->render('elements/_searchbox'); ?>
      </div>
    </div>
    <? }

    } else { ?>
    <h2>Search:</h2>
    <div uk-grid class="uk-flex uk-flex-center">
      <div class="uk-width-1-1 uk-width-1-2@m">
        <?= $files->render('elements/_searchbox');  ?>
      </div>
    </div>
    <? } ?>
  </div>
</main>

Explanation:

This part here at the top of the template handles the requests that are send via ajax. This is the important part for later on.

<?php

if($config->ajax) {
    // Return search results in JSON format
    $q = $sanitizer->selectorValue($input->get->q);
    $results = $pages->find('search_cache%=' . $q);; // Find all pages and save as $results
    header("Content-type: application/json"); // Set header to JSON
    echo $results->toJSON(); // Output the results as JSON via the toJSON function
    return $this->halt();
}
?>

What this does:

  1. check if the current request is an ajax request
  2. if so, search inside the search cache field
  3. return the results in JSON format
  4. then quit processing the rest of the template (we don’t want to render any markup in that case!)

Everything below this part is the normal search template logic. If you send a search request via a form somewhere on the website you want to be redirected to the result page and all the results will be listed on the page just like you would expect.

2. Make the search „work“

Create a template file called "_searchbox.php" (this file is also included in the search template code above, so adjust your paths/names accordingly).

To make search requests you want to include this search form anywhere on your page.

_searchbox.php:

<div class="search-wrapper uk-padding uk-text-center">
    <h4>Searchbox</h4>
    <form class="searchform uk-position-relative uk-flex" method="get" action="<?= $pages->get("template=search")->url ?>">
        <input class="uk-input" id="searchInput" name="q" type="search" aria-label="Suchen" autocomplete="off" placeholder="z.B. Bagger">
        <button class="uk-button uk-button-primary search-button" type="submit">
            <span class="uk-visible@s">Search</span>
        </button>
        <div id="suggestions" class="uk-box-shadow-medium">
        </div>
    </form>
</div>

With this simple search form you should now be able to do a basic search that leads you to a search result page.

3. Make the ajax-search „work“

 

Now comes the interesting part. We will add in a java script snippet into our just created _searchbox.php that sends ajax requests to the search template page while we are typing into the search intput field and it will display the results in a nice little dropdown. 

Feel free to adjust the code to your needs!

<script>
  document.addEventListener("DOMContentLoaded", () => {

    const searchForm = document.querySelector('.searchform');
    const searchUrl = searchForm.getAttribute('action');

    const searchInput = document.getElementById('searchInput');
    const suggestionsDiv = document.getElementById('suggestions');
    let selectedSuggestionIndex = -1;

    // close the auto-complete container when clicked outside the element
    document.addEventListener('click', function(event) {
      if (!suggestionsDiv.contains(event.target)) {
        suggestionsDiv.style.display = 'none';
      }
    });

    searchInput.addEventListener('input', () => {

      const searchText = searchInput.value.toLowerCase();

      // Immediately Invoked Function Expression (IIFE)
      (async () => {

        try {
          const response = await fetch(searchUrl+"?q="+searchText, {
            headers: {
              'X-Requested-With': 'XMLHttpRequest'
            }
          });

          if (response.status === 200) {
            const data = await response.json();
            showResults(data);
          }
          else {
            console.log(response.status);
            console.log(response.statusText);
          }

        } catch (error) {
          console.error(error);
        }
      })();

      function showResults(data) {

        // Show suggestions only if the input has at least two characters
        if (searchText.length >= 2) {

          const suggestionHTML = data.map(item => {
            // Highlight the matching characters using a <span> element with a CSS class
            const highlightedTitle = item.title.replace(
              new RegExp(searchText, 'gi'),
              match => `<span class="highlight">${match}</span>`
            );
            return `<li class="suggestion"><a href="${item.url}">${highlightedTitle}</a></li>`;
          }).join(''); // Array to string conversion

          suggestionsDiv.innerHTML = ''; // Clear the suggestions if input length is less than two characters

          // create list and append search results
          const suggestionList = document.createElement("ul");
          suggestionList.classList.add('suggestion-list', 'uk-list');
          suggestionList.innerHTML = suggestionHTML;

          suggestionsDiv.appendChild(suggestionList);

          selectedSuggestionIndex = -1;

          // show the results
          suggestionsDiv.style.display = "block";
        } else {
          suggestionsDiv.innerHTML = ''; // Clear the suggestions if input length is less than two characters
        }
      }

    });

    // Event listener for arrow key presses
    searchInput.addEventListener("keydown", function (event) {
      const suggestions = document.querySelectorAll(".suggestion");

      if (event.key === "ArrowDown") {
        event.preventDefault();
        selectedSuggestionIndex = Math.min(
          selectedSuggestionIndex + 1,
          suggestions.length - 1
        );
      } else if (event.key === "ArrowUp") {
        event.preventDefault();
        selectedSuggestionIndex = Math.max(selectedSuggestionIndex - 1, -1);
      } else if (event.key === "Enter") {
        event.preventDefault();

        if (selectedSuggestionIndex === -1) {
          searchForm.submit();
        }
        else {
          window.location = suggestions[selectedSuggestionIndex].querySelector('a').href;
        }
      }

      // Highlight the selected suggestion
      suggestions.forEach((suggestion, index) => {
        suggestion.classList.toggle(
          "selected",
          index === selectedSuggestionIndex
        );
      });
    });
  });

</script>

Keep in mind that you need some CSS styes to make it look good and actually work. These are my styles(in LESS format) but feel free to write you own stylesheet for this.

search.less:

.search-wrapper {
  position: relative;
  background: @secondary-blue;

  h4 {
    color: @primary-blue;
  }

  .highlight {
    color: @primary-red;
  }

}

#suggestions {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 10;
  background: @white;

  .suggestion-list {
    margin: 0;

    li {
      transition: background-color 150ms ease-in-out;

      a {
        padding: 10px;
        display: block;
        text-align: left;

        &:hover,
        &:focus,
        &:active {
          text-decoration: none;
        }
      }

      &:hover,
      &:focus,
      &:active,
      &.selected {
        background: @secondary-blue;
      }
    }

    li + li {
      border-top: 1px solid @secondary-blue;

    }
  }
}

That's it!

Again feel free to adjust all of the code to your needs. If you have any suggestions how to achieve things a bit easier just let me know.

  • Like 16
  • Thanks 3
Link to comment
Share on other sites

  • 1 month later...

Hello and many thanks for this tutorial. I adapted it to my site and it works great.

There is one thing. I tried to search some content that is inside a Repeater Matrix field, but it seems, this isn't possible so far?
I have a Repeater Matrix field on every page, where I can add a text field, a headline, an image field, and so on.
The search routine wasn't able to find anything, that's inside it.
I believe it's not that simple to just add the Repeater Matrix field to the search_cache field, regenerate the cache and be ready, since I already tried that out. 🙂

So, I'm a little bit clueless what needs to be changed in the code to work with Repeater Matrix too.
Any suggestions?

Thanks so much,
Thomas

Link to comment
Share on other sites

20 hours ago, chuckymendoza said:

Hello and many thanks for this tutorial. I adapted it to my site and it works great.

I am glad that it's working for you!

I did a quick test with repeater matrix items and as you described it seems not to work with that. I don't use the Repeater Matrix field anymore (instead the RockPageBuilder from @bernhard) but I haven't tried the autocomplete search with that module either!

Link to comment
Share on other sites

You can just add a hidden field that is populated on page save from the content of each RepeaterMatrix or RockPageBuilder item. That field can then be queried by any search.

In RockPageBuilder you get the benefit that you can simply add a dedicated method to every block:

<?php
// Text.php
public function searchIndex() {
  return $this->body;
}

// Accordion.php
public function searchIndex() {
  $index = "";
  foreach($this->items as $item) {
    $index .= $item->headline . ": " . $item->body . "\n";
  }
  return $index;
}

Thats not a built in feature but as all RockPageBuilder blocks are dedicated PHP files and classes things like this are super easy and clean to implement.

This solution would obviously add additional data to the database, so fields are not queried directly. But that adds more options in presenting your search results. See here for some insights:

 

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...