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

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • 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?
       

    • By sww
      Hey there,
      i am trying to add a kind of "intelligent" search.
      Is there any way to ignore punctuation and extra chars.
      e.g.
      When I type "somebody elses" that I still get the result "SOMEBODY ELSE´S CAR, 2005"
      The problem is the extra ´ … if I don't type it I don't get the result.
      So far I am using %=query (which isn't enough, I know)
      So the idea would be something like that:
      $selector = "sanitize(title|text)%=$q, limit=50";
      I know, it's not gonna work like that … just to illustrate.
      Best,
      Stefan
    • By Robin S
      After forgetting the class name of the wonderful AdminPageFieldEditLinks module for what feels like the 100th time I decided I needed to give my failing memory a helping hand...
      Autocomplete Module Class Name
      Provides class name autocomplete suggestions for the "Add Module From Directory" and "Add Module From URL" fields at Modules > New.
      Requires ProcessWire >= v3.0.16.
      Screencast

      Installation
      Install the Autocomplete Module Class Name module.
      Configuration
      Add Module From Directory
      Choose the type of autocomplete suggestions list: "Module class names from directory" or "Custom list of module class names". The latter could be useful if you regularly install some modules and would prefer a shorter list of autocomplete suggestions. The list of class names in the modules directory is generated when the Autocomplete Module Class Name module is installed. It doesn't update automatically (because the retrieval of the class names is quite slow), but you can use the button underneath when you want to retrieve an updated list of class names from the directory. Add Module From URL
      If you want to see autocomplete suggestions for the "Add Module From URL" field then enter them in the following format:
      [autocomplete suggestion] > [module ZIP url]
      Example: RepeaterImages > https://github.com/Toutouwai/RepeaterImages/archive/master.zip Awesomplete options
      The "fuzzy search" option uses custom filter and item functions for Awesomplete so that the characters you type just have to exist in the autocomplete suggestion item and occur after preceding matches but do not need to be contiguous. Uncheck this option if you prefer the standard Awesomplete matching. Custom settings for Awesomplete can be entered in the "Awesomplete options" field if needed. See the Awesomplete documentation for more information.  
      https://github.com/Toutouwai/AutocompleteModuleClassName
      https://modules.processwire.com/modules/autocomplete-module-class-name/
    • By louisstephens
      I have been messing around with creating pages from ajax requests, and it has gone swimmingly thus far. However, I am really struggling with creating a page and saving an image via ajax. 
      The form:
      <form action="./" role="form" method="post" enctype="multipart/form-data"> <div> <input type="text" id="preview" name="preview" placeholder="Image Title"> </div> <div> <input type="file" id="preview-name" name="preview-name"> </div> <div> <select id="select-tags" name="select-tags"> <?php $tags = $pages->find("template=tag"); ?> <option value="">Select Your Tags</option> <?php foreach ($tags as $tag) : ?> <option value="<?= $tag->name; ?>"><?= $tag->name; ?></option> <?php endforeach; ?> </select> </div> <div> <button type="button" id="submit-preview" name="submit" class="">Upload Images</button> </div> </form>  
      The ajax in my home template:
      $('#submit-preview').click(function(e) { e.preventDefault(); title = $("#preview").val(); image = $("input[name=preview-name]"); console.log(title); console.log(image); data = { title: title, image: image //not sure if this is actually needed }; $.ajax({ type: 'POST', data: data, url: '/development/upload-preview/', success: function(data) { console.log("Woo"); }, error: function(xhr, ajaxOptions, thrownError) { alert(xhr.responseText); } }); }); And finally in my ajax template:
      $imagePath = $config->paths->assets . "files/pdfs/"; //was from an older iteration $title = $sanitizer->text($_POST['title']); $image = $sanitizer->text($_POST['image']); $p = new Page(); $p->template = "preview"; $p->parent = $pages->get("/previews/"); $p->name = $title; $p->title = $title; $p->save(); $p->setOutputFormatting(false); $u = new WireUpload('preview_image'); $u->setMaxFiles(1); $u->setOverwrite(false); $u->setDestinationPath($p->preview_image->path()); $u->setValidExtensions(array('jpg', 'jpeg', 'gif', 'png', 'pdf')); foreach($u->execute() as $filename) { $p->preview_image->add($filename); } $p->save(); I can complete the file upload but just using a simple post to the same page and it it works well, but I was really trying to work out the ajax on this so I could utilize some modals for success on creation (and to keep my templates a little cleaner). When I do run the code I have, a new/blank folder is created under assets, and a new page is created with the correct title entered. However, no image is being processed. I do get a 200 status in my console. I have searched google for help, but everything seems to be slightly off from my needs. If anyone could help point me in the right direction I would greatly appreciate it. 
    • By louisstephens
      So I am using ajax to upload an image, but I am getting the error "Method WireUpload:: save does not exist or is not callable". I am not quite sure how to go about fixing this (at the moment).
      elseif($config->ajax && $input->urlSegment1 == "upload-preview") { $u = $config->paths->assets . "files/pdfs/"; $title = $sanitizer->text($_POST['title']); $p = new Page(); $p->template = "preview"; $p->parent = $pages->get("/previews/"); $p->name = $title; $p->title = $title; $p->save(); $p->setOutputFormatting(false); $u = new WireUpload('preview_image'); $u->setMaxFiles(1); $u->setOverwrite(false); $u->setDestinationPath($p->preview_image->path()); $u->setValidExtensions(array('jpg', 'jpeg', 'gif', 'png', 'pdf')); foreach($u->execute() as $filename) { $p->preview_image->add($filename); } $u->save(); } I compared my code to something I did previously (though previously I just posted to the current template file, not through ajax) which works, but this doesnt seem to be working. I have the _init.php file prepending as well. Does anyone have any ideas of what might be happening?
×
×
  • Create New...