Jump to content

Ajax autocomplete search using jQuery Typehead


AndZyk
 Share

Recommended Posts

Hello everyone,

I always wanted to try out an ajax autocomplete search function, but never knew where to start. Of course there is the Ajax Page Search module by soma, but it seems that it was build around the basic site profile. In my case I wanted something more custom and I discovered in this thread the jQuery Plugin Typeahead by RunningCoder, which seemed to be nice. After many hours figuring out, how to combine this Plugin with ProcessWire, I finally got it implemented and want to share my solution with anyone, who also struggles with this topic.

1. Set-Up Typeahead

Download the Typeahead-Plugin from the website (I prefer via Bower) and include the following scripts and stylesheets in your templates:

<html>
<head>
	...
	<!-- Optional CSS -->
	<link rel="stylesheet" href="/vendor/jquery-typeahead/dist/jquery.typeahead.min.css">
	
	<!-- Required JavaScript -->
	<script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
	<script src="/vendor/jquery-typeahead/dist/jquery.typeahead.min.js"></script>
	...
</head>

As next step we need the JSON data.

2. Install Pages to JSON

To get the necessary data of all pages as JSON, I use the module Pages to JSON, which provides an easy way to output pages as JSON objects. Of course you can achieve this without this module, but I am not very experienced with JSON, so this module was really helpful.

After you downloaded and installed the module, you can configure in the module settings page, which fields you want to output. You can select between own and system fields. For my purpose I selected only title and url to be outputted.

3. Output JSON

Now, that we have the module configured, we have to output our search suggestions as JSON. I did it in my template file search.php like this:

<?php namespace ProcessWire;

// Check if ajax request
if($config->ajax) {

	$results = $pages->find("has_parent!=2"); // Find all published 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

} else {

	// Your own front-end template

}

To sum up, we search the pages we want as search suggestions and save them in a variable. Then we output them with the toJSON-Function by the Pages to JSON-Module. All of this happens in a Ajax-Request, that is the reason why we check first, if the page is called via an Ajax request.

4. Insert Form

We can now embed the HTML form anywhere you want. Either in an header-include or a specific template. Also you can use your own classes, for this example I used the Typeahead-demo-mark-up and extended it a little.

<form id="searchform" method="get" action="<?= $pages->get("template=search")->url ?>">
	<div class="typeahead__container">
		<div class="typeahead__field">

			<span class="typeahead__query">
				<input id="q"
				       name="q"
				       type="search"
				       placeholder="Search"
			               autocomplete="off">
			</span>

			<span class="typeahead__button">
				<button type="submit">
					<span class="typeahead__search-icon"></span>
				</button>
			</span>

		</div>
	</div>
</form>

The action-attribute in the form-tag is the url of your search-site. This attribute is of course necessary to know where the form redirects you and where the JSON data is located.

5. Initialize Typeahead

As last step we have to initialize the Typeahead-plugin jQuery like this:

$(document).ready(function() {

	var actionURL = $('#searchform').attr('action'); // Save form action url in variable

	$.typeahead({
		input: '#q',
		hint: true,
		display: ["title"], // Search objects by the title-key
		source: {
			url: actionURL // Ajax request to get JSON from the action url
		},
		callback: {
			// Redirect to url after clicking or pressing enter
			onClickAfter: function (node, a, item, event) {
				window.location.href = item.url; // Set window location to site url
			}
		}
	});

});

We save the action url of the form in a variable, then we initialize Typeahead by selecting the input-field inside the form. As the source we can pass the action url and I included the callback, to link the search results with the site urls.

Now you should have a nice ajax autocomplete search form, which of course you can further style and configure. I hope I didn't forget anything, but if so, please let me know.  :)

Regards, Andreas

  • Like 16
Link to comment
Share on other sites

I'm happy you find this tutorial useful.

But since this topic is specific for the front-end and uses one of many plugins, I don't think this should be covered in the documentation. Because after all, ProcessWire doesn't dictate you how you should do things in the front-end.  ;)

  • Like 2
Link to comment
Share on other sites

  • 2 months later...

The problem with your approach is, that it only requests the data once and ProcessWire returns all pages instead of those who match the query string.
This could be a problem on very dynamic sites, where the content changes often.
 
Here is my solution which solves this:
 
Modify the standard search.php and add

if ($config->ajax) {
        header("Content-type: application/json"); // Set header to JSON
        echo $matches->toJSON(); // Output the results as JSON via the toJSON function
    } 

so the whole file reads

<?php namespace ProcessWire;

// look for a GET variable named 'q' and sanitize it

$q = $sanitizer->text($input->get->q);
// did $q have anything in it?
if ($q) {
    // Send our sanitized query 'q' variable to the whitelist where it will be
    // picked up and echoed in the search box by _main.php file. Now we could just use
    // another variable initialized in _init.php for this, but it's a best practice
    // to use this whitelist since it can be read by other modules. That becomes
    // valuable when it comes to things like pagination.
    $input->whitelist('q', $q);

    // Sanitize for placement within a selector string. This is important for any
    // values that you plan to bundle in a selector string like we are doing here.
    $q = $sanitizer->selectorValue($q);

    // Search the title and body fields for our query text.
    // Limit the results to 50 pages.
    $selector = "title|body%=$q, limit=50";

    // If user has access to admin pages, lets exclude them from the search results.
    // Note that 2 is the ID of the admin page, so this excludes all results that have
    // that page as one of the parents/ancestors. This isn't necessary if the user
    // doesn't have access to view admin pages. So it's not technically necessary to
    // have this here, but we thought it might be a good way to introduce has_parent.
    if ($user->isLoggedin()) $selector .= ", has_parent!=2";

    // Find pages that match the selector
    $matches = $pages->find($selector);

    $cnt = $matches->count;
    // did we find any matches?
    if ($cnt) {

        // yes we did: output a headline indicating how many were found.
        // note how we handle singular vs. plural for multi-language, with the _n() function
        $content = "<h2>" . sprintf(_n('Found %d page', 'Found %d pages', $cnt), $cnt) . "</h2>";

        // we'll use our renderNav function (in _func.php) to render the navigation
        $content .= renderNav($matches);

    } else {
        // we didn't find any
        $content = "<h2>" . __('Sorry, no results were found.') . "</h2>";
    }
    if ($config->ajax) {
        header("Content-type: application/json"); // Set header to JSON
        echo $matches->toJSON(); // Output the results as JSON via the toJSON function
    }
} else {
    // no search terms provided
    $content = "<h2>" . __('Please enter a search term in the search box (upper right corner)') . "</h2>";
}

Then call typeahead with the following options:

$.typeahead({
        input: '#q',
        order: 'desc',
        hint: false,
        minLength: 3,
        //cache: false,
        accent: true,
        display: ['title'], // Search objects by the title-key
        backdropOnFocus: true,
        dynamic: true,
        backdrop: {
            "opacity": 1,
            "background-color": "#fff"
        },
        href: "{{url}}",
        emptyTemplate: "No results for {{query}}",
        searchOnFocus: true,
        cancelButton: false,
        debug: true,
        source: {
            //url: actionURL // Ajax request to get JSON from the action url
            ajax: {
                method: "GET",
                url: actionURL,
                data: {
                    q: '{{query}}'
                },
            }
        },
        callback: {
            onHideLayout: function (node, query) {
                $('#searchform').hide();
                console.log('hide search');
            }
        }
    });

The important parts are "dynamic:true" and the "source" configuration so the query string is beeing sent.
 
Now you have a nice AJAX search.
 
EDIT: If you also want to find the query string in other fields than the title make sure you add 

filter: false

to the config of typeahead.

Edited by jmartsch
  • Like 4
  • Thanks 4
Link to comment
Share on other sites

  • 3 years later...

I am using exactly this code, I am getting from PHP 12 results but the autocomplete always shows 8, doesnt matter if I get 20, 30, 40 I always get 8 on visual,

Here is my Js

$(document).ready(function()
{
  $('#autocompletar_buscar_producto_por_nombre .typeahead').typeahead(
  {
    hint: true,
    highlight: true,
    minLength: 1,
    maxLength: false,
    limit: Infinity,
    source: function (query, result)
    {
      $.ajax({
        url: "../PHP/autocompletar_info_producto_existente.php",
        data: 'query=' + query,
        dataType: "json",
        type: "POST",
        beforeSend: function()
            {
          $('#div_estado_consultar_producto_por_nombre').html("Consultando producto...");
        },
        success: function (data)
        {
          if(data=="producto no encontrado"){$('#div_estado_consultar_producto_por_nombre').html("Producto no encontrado");return;}
          result($.map(data, function (item)
          {
            $('#div_estado_consultar_producto_por_nombre').html("");
            return item;
          }));
        }
      });
    }
  });
});

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.

  • Similar Content

    • By Liam88
      Hi,
      I'm really struggling with this as it's something not in my wheelhouse. I'm creating a blog style page (a grid of cards) which has attributes.
      I have a snip of javascript which grabs values from checkboxes which are put into a value like the below:
      ?content=static_video&channel=facebook-ads_instagram-ads
      document.querySelector("form").onsubmit=ev=>{ ev.preventDefault(); let o={}; ev.target.querySelectorAll("[name]:checked").forEach(el=>{ (o[el.name]=o[el.name]||[]).push(el.value)}) console.log(location.pathname+"?"+ Object.entries(o).map(([v,f])=> v+"="+f.join("_")).join("&") ); document.location.href = location.pathname+"?"+ Object.entries(o).map(([v,f])=> v+"="+f.join("_")).join("&"); } As I'm currently refeshing the page on button click with those values the end result includes the location but can easily remove this.
      I then use this value in "input->get" to get the values which I then append to a find() rule. See code below:
      $selector = "template='adbank_pages',sort=published,include=all,status!=hidden"; // Get the channel and content inputs $channel = $input->get->channel; $content = $input->get->content; if($channel){ // Grab the channel string, explode into an array for checkbox checking and then replace _ with | to create or rules in the selector. $chanArray = explode("_", $channel); $chan = $channel = str_replace('_', '|', $channel); $selector = $selector .= ",ab_channels=$chan"; } if($content){ // Grab the content string, explode into an array for checkbox checking and then replace _ with | to create or rules in the selector. $contArray = explode("_", $content); $cont = $content = str_replace('_', '|', $content); $selector = $selector .= ",ab_content=$cont"; } if($input->get){ // If a valid input result $all = $pages->find($selector); } }else{ // If no input show them all $all = $page->children("template='adbank_pages',sort=-published,include=all,status!=hidden"); } $items = $all->find("limit=12"); // Limit the output and use pagination As mentioned above I currently refresh the page to adjust the $selector filter within the $all with a fallback $all if there are no results.
      I know I need to use AJAX to filter the content without refresh but I am really struggling with the set up. I have read multiple posts including the original by Ryan but still confused.
      If anyone can direct/help on this it would be appreciated.
      Thank you
    • By donatas
      Hello,
      how would I do a multi-language website search with just a selector?
      I have many multi-lang fields and I want to do a search through all of them at once and through all of their language values.
      Is there a "selector way" of doing this? Maybe something like `title|title:de|title:it`? It seems I have seen this somewhere a long time ago but can't find in any documentation or forum search...
      Or the only way of doing it is by running separate searches for each language with output formatting off and then consolidating it all in one single results array?
      Because I still want to give users a result, even if it is in another language than current $user. Visitors mostly will be searching for specific terms that are very similar in all languages, but might be not used in one language version of a single page, for example. Or the user might not have switched language tohis prefered and did the search first, etc.. (many use cases in my situation)
      Example:
      $pages->find('title~='.$q) - maybe different operator is needed? /en/search/?q=visit = 1 results /it/search/?q=visit = 0 results Thanks for any advice!
    • By sebr
      Hi
      In my search page, I used a selector like this :
      $searchQuery = $sanitizer->entities($input->get('q')); $searchQuery = $sanitizer->selectorValue($searchQuery); $selector = 'title|subtitle|summary|html_body_noimg~=' . $searchQuery; $matches = $pages->find($selector); I don't have the same results if $searchQuery contains accent or not.
      For example,
      with « bâtiment » I have no result with « batiment » I have onea result : « Les bâtiments et les smart-city » Normally I should have the same results? How can I do that ?
      Thanks for your help
    • By michelangelo
      Hello guys, I am building a sort of an archive. Relatively simple, although I have about 8000 records, each with 15 fields (text, int, images, url). I created a crude search system with a form (emulating the famous Skyscrapper example) to filter through the system. Everything works but it is quite slow... I have 2 questions which are related:

      1. How can I search through the database?
      2. What is a good practice to display many records like these?
      -----------------------------------------
      1. I am retrieving the results with
      $songs = $pages->findMany('template=nk-song'); Then I do a foreach to render them all. I am unsure if that is a good way. If I render all of them on the page, it creates thousands of divs with a bit of text, and this can take a while (10s-15s).
       
      2. This one is even worse :D as every time I retrieve my desired records with something like this:
      $page->find("field_to_search_through~=my_query_string") I get between 20 and 200, but when I render them I am creating iframes with YouTube videos and that can take up to 10s to finish. I "solved" it by only loading the iframes if they are in view with IntersectionObserver on the client-side. But I feel there is a more precise PHP / ProcessWire approach.
       
      Just to clarify, I started doing all of this custom rendering and querying because tools like ElasticSearch or SearchEngine were a bit complicated and I needed a simple to retrieve information and then display it in my own way.
      Thank you!
    • By ICF Church
      Hi 👋
      Anyone else having this problem?
      Requirements:
      - Repeater (matrix & normal) with mutlilanguage fields (text, textarea…) 
      - Backend language set to something other than default (ie. German) 
      Reproduce:
      - Add a new repeater Item (ajax, I found no way to possible to disable it with matrix)

      (Notice how the default language tab is active instead of the backend language…)
      - Write something into the (default language) field
      - Try to save, if field is required, this will not work. If not required, then when reloading, the content will be inside the backend language field, instead of the default language field who was (presumably) active
      Analysis:
      When  loading  a new repeater element with ajax, the default langue tab is active, but the backend language inputfield is visible (with no visual indication). When writing into the field, it will populate the backend language. When manually clicking on the default language tab (which is already active), the field will switch to the actual default language field (which is [now] empty) (that can now be populated…)
      Also Notice, the labels of the elements to be added are in default language as well instead of the translated label (images instead of Bilder)…
      ProcessWire 3.0.148, Profields 0.0.5…
      Is it my system configuration, or does anyone else have the same issue? This is a screen recording of the problem:
      Issue: https://github.com/processwire/processwire-issues/issues/1179

      Screen Recording 2020-02-25 at 14.18.31.mov
×
×
  • Create New...