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

×
×
  • Create New...