Stefanowitsch Posted December 13, 2023 Share Posted December 13, 2023 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: 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/ First of all, go to Modules and install a Core module called FieldtypeCache. Add a new field “search_cache” and select “Cache” as it’s type. Save the field. On the Details tab choose the fields you want to make searchable. Save again. Add “search_cache” to all templates you want to include in your search results. 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: 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. 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: check if the current request is an ajax request if so, search inside the search cache field return the results in JSON format 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. 16 3 Link to comment Share on other sites More sharing options...
chuckymendoza Posted January 30 Share Posted January 30 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 More sharing options...
Stefanowitsch Posted January 31 Author Share Posted January 31 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 More sharing options...
chuckymendoza Posted January 31 Share Posted January 31 Ah, okay, I see. Okay, good to know, that I didn't made any mistakes in my implementation. Would be nice to see your AJAX-Search working with RockPageBuilder too, in the future. ? Thanks for the quick response, Thomas Link to comment Share on other sites More sharing options...
bernhard Posted January 31 Share Posted January 31 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 More sharing options...
gornycreative Posted February 2 Share Posted February 2 I believe SearchEngine is able to index content within repeaters and add it to the searchindex field it creates. https://processwire.com/modules/search-engine/ It can also return JSON so it may be a drop in alternative or work side-by-side with the FieldtypeCache? Regarding repeater detection and indexing: 1 Link to comment Share on other sites More sharing options...
chuckymendoza Posted February 2 Share Posted February 2 Thanks a lot, that’s a good tip, I will have a deeper look at! Thomas Link to comment Share on other sites More sharing options...
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now