Jump to content
AndZyk

Ajax autocomplete search using jQuery Typehead

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

Share this post


Link to post
Share on other sites

Thank you for the explanation.  It would be nice if there was some processwire documentation that covered stuff like this instead of having to lurk/dig through years of forum posts  :)

  • Like 1

Share this post


Link to post
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

Share this post


Link to post
Share on other sites

With Version 2.5.0 they changed the class names of the HTML form to "typeahead__". I updated it in my original post, just so nobody gets confused.  ;)

  • Like 3

Share this post


Link to post
Share on other sites

Thanks AndZyk ... this is what I'm looking for my frontend  :)

  • Like 1

Share this post


Link to post
Share on other sites

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 2

Share this post


Link to post
Share on other sites

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;
          }));
        }
      });
    }
  });
});

Share this post


Link to post
Share on other sites

@Gabriel Uribe Take a look at the configuration of the typeahead script. There you find a setting "maxItem" where 8 seems to be default (not checked). Set it to 0 or false to display all results, or to the number you would like to display.

Share this post


Link to post
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

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • 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
    • By snobjorn
      I have a website with multiple content types that I want to be accessible through search. I really like the live search on processwire.com, that sorts content types while typing. I tried to find the code to recreate this, with no luck. Does anyone know if this is jquery, specific jquery plugins, json/xml cached files, and what kind of PHP code is used? Any tip that point me in the right direction would be much apperciated.
      The search result listing seems fairly easy to create with sorting through parameters.
    • By Anders
      I want to allow full text search on my site. There is a very nice solution that comes right out of the box:
      $selector = "title|body~=$q, limit=50"; This works, but to make it even better I would want to give higher weight to pages where the search term occurs in the title, than if it just occurs in the body. After all, a page with the title "Wine from France" is probably the best match for the search "france wine". How do I accomplish this in ProcessWire?
      I can see three possible paths, but I am not very fond of any of them:
      Do a direct SQL query, circumventing the API, along these lines. But I would prefer to abstract away the database layout if at all possible. Use something like ElasticSearch, but to be honest that would be to complicated to set up and maintain in the long run. Make multiple lookups, first for matches in the title, then for matches in the body, and merge and sort in PHP. My suspicion is that this would get complicated quite quickly. For instance, how do you deal with a page that has two of the three search terms in the title and the third in the body? Is there a magic option four I should look into? Or are any of the above options better than the others? Any input is welcome!
    • By jds43
      Hello,
      I have a search page loosely based on Skyscrapers where I'm parsing a selector with options 'beds', 'bathrooms', 'size' fields. It is working well until I select 'Any' after I've run a search. This is where no results are returned (/?beds=&bathrooms=&size=&submit=). I want it to reset and show all results.
      I hope this isn't too vague.
       
    • By michelangelo
      Hello there,
      I am building my website, which has a dozen projects with 10 images each. Basically, I need a filtering system but built in the most efficient and user-friendly way. You can see below that the images flow sideways so being hidden, JS lazy loading was a good tool, but I just wanted to try AJAX. Is it fit for this purpose or it's more for dynamic content?
       

×
×
  • Create New...