3 Steps for Creating AJAX Post Filters

AJAX Posts (or Custom Post Types) Filter for your website by the Date, by Categories (Taxonomies) or by the Custom Field Values. Ascending or Descending order direction.

#admin-ajax.php, #noplugins  /  June 2  /   203

So many people asks me about post filters. So I decided to write a simple post about it — so everyone could understand how it works. 

In this post I will show you how to create an asynchronous filter by yourself, which allows to filter posts by taxonomy terms, meta values and sort results by date.

Step 1. Everything begins with the HTML form #

Our filter form will consist of 4 parts. I’ll describe each part of the form separately.

I want to filter posts by taxonomy terms #

First part of the form is a dropdown <select> of taxonomies. To create that <select> you may freely use get_terms() function. This function can work not only with custom taxonomies but with default categories and tags as well.

if( $terms = get_terms( 'category', 'orderby=name' ) ) : // to make it simple I use default categories
	echo '<select name="categoryfilter"><option>Select category...</option>';
	foreach ( $terms as $term ) :
		echo '<option value="' . $term->term_id . '">' . $term->name . '</option>'; // ID of the category as the value of an option
	endforeach;
	echo '</select>';
endif;

I want to filter posts by custom field values as well #

I use range of prices. In our case price is a custom field value that is stored in wp_postmeta table under the _price key in database.

<input type="text" name="price_min" placeholder="Min price" />
<input type="text" name="price_max" placeholder="Max price" />

By date: Ascending or Descending #

Simple radio buttons will help us to sort posts in ascending or descending order.

<label>
	<input type="radio" name="date" value="ASC" /> Date: Ascending
</label>
<label>
	<input type="radio" name="date" value="DESC" selected="selected" /> Date: Descending
</label>

Actually the featured image is just an attachment ID that is stored like a custom field value under _thumbnail_id key. We will just check that it exists.

<label>
	<input type="checkbox" name="featured_image" /> Only posts with featured image
</label>

Complete form code #

You may skip all the previous field description and use the code below as is. Insert it anywhere you want the filter to be.

<form action="<?php echo site_url() ?>/wp-admin/admin-ajax.php" method="POST" id="filter">
	<?php
		if( $terms = get_terms( 'category', 'orderby=name' ) ) : // to make it simple I use default categories
			echo '<select name="categoryfilter"><option>Select category...</option>';
			foreach ( $terms as $term ) :
				echo '<option value="' . $term->term_id . '">' . $term->name . '</option>'; // ID of the category as the value of an option
			endforeach;
			echo '</select>';
		endif;
	?>
	<input type="text" name="price_min" placeholder="Min price" />
	<input type="text" name="price_max" placeholder="Max price" />
	<label>
		<input type="radio" name="date" value="ASC" /> Date: Ascending
	</label>
	<label>
		<input type="radio" name="date" value="DESC" selected="selected" /> Date: Descending
	</label>
	<label>
		<input type="checkbox" name="featured_image" /> Only posts with featured image
	</label>
	<button>Apply filter</button>
	<input type="hidden" name="action" value="myfilter">
</form>
<div id="response"></div>

Some comments:

  • Line #1. I use default WordPress function site_url() to get actual website URL.
  • Line #1. admin-ajax.php is the default WordPress AJAX processor. I place it into the form action attribute just for simplicity.
  • Line #23. Hidden input field with the myfilter attribute is required — this is how WordPress recognize what function to use.
  • Line #25. #response div element is the container where the code will paste the result data.

Step 2. jQuery script to Send a Request and to Receive Result Data #

In this part of the post I suppose that you know something about jQuery, at least how to include it to a website page. Here is the complete jQuery-based processing code. It will send the request when the form is submitted.

jQuery(function($){
	$('#filter').submit(function(){
		var filter = $('#filter');
		$.ajax({
			url:filter.attr('action'),
			data:filter.serialize(), // form data
			type:filter.attr('method'), // POST
			beforeSend:function(xhr){
				filter.find('button').text('Processing...'); // changing the button label
			},
			success:function(data){
				filter.find('button').text('Apply filter'); // changing the button label back
				$('#response').html(data); // insert data
			}
		});
		return false;
	});
});

Step 3. PHP code to Process the Request #

I think it is the most interesting part. In this part you decide how to filter the posts the best way. This code is fully based on WP_Query.

function misha_filter_function(){
	$args = array(
		'orderby' => 'date', // we will sort posts by date
		'order'	=> $_POST['date'] // ASC или DESC
	);
 
	// for taxonomies / categories
	if( isset( $_POST['categoryfilter'] ) )
		$args['tax_query'] = array(
			array(
				'taxonomy' => 'category',
				'field' => 'id',
				'terms' => $_POST['categoryfilter']
			)
		);
 
	// create $args['meta_query'] array if one of the following fields is filled
	if( isset( $_POST['price_min'] ) && $_POST['price_min'] || isset( $_POST['price_max'] ) && $_POST['price_max'] || isset( $_POST['featured_image'] ) && $_POST['featured_image'] == 'on' )
		$args['meta_query'] = array( 'relation'=>'AND' ); // AND means that all conditions of meta_query should be true
 
	// if both minimum price and maximum price are specified we will use BETWEEN comparison
	if( isset( $_POST['price_min'] ) && $_POST['price_min'] && isset( $_POST['price_max'] ) && $_POST['price_max'] ) {
		$args['meta_query'][] = array(
			'key' => '_price',
			'value' => array( $_POST['price_min'], $_POST['price_max'] ),
			'type' => 'numeric',
			'compare' => 'between'
		);
	} else {
		// if only min price is set
		if( isset( $_POST['price_min'] ) && $_POST['price_min'] )
			$args['meta_query'][] = array(
				'key' => '_price',
				'value' => $_POST['price_min'],
				'type' => 'numeric',
				'compare' => '>'
			);
 
		// if only max price is set
		if( isset( $_POST['price_max'] ) && $_POST['price_max'] )
			$args['meta_query'][] = array(
				'key' => '_price',
				'value' => $_POST['price_max'],
				'type' => 'numeric',
				'compare' => '<'
			);
	}
 
 
	// if post thumbnail is set
	if( isset( $_POST['featured_image'] ) && $_POST['featured_image'] == 'on' )
		$args['meta_query'][] = array(
			'key' => '_thumbnail_id',
			'compare' => 'EXISTS'
		);
 
	$query = new WP_Query( $args );
 
	if( $query->have_posts() ) :
		while( $query->have_posts() ): $query->the_post();
			echo '<h2>' . $query->post->post_title . '</h2>';
		endwhile;
		wp_reset_postdata();
	else :
		echo 'No posts found';
	endif;
 
	die();
}
 
 
add_action('wp_ajax_myfilter', 'misha_filter_function'); 
add_action('wp_ajax_nopriv_myfilter', 'misha_filter_function');

Now you know the basics and you can easily create filters like this on WordPress:

Product filter example from eBay

If you still have problems with filters, please contact me and I will help you or leave a comment below.

Only the best of WordPress

Subscribe to this weekly newsletter to receive the latest blog posts by email.I respect your privacy. Your email is safe with me.

Comments 203

← Older
  • Hello Misha, Thanks for the great tutorial.
    I sent you an email but made some progress, and maybe my issue can help others ?

    I modified the code to submit the form on input change.

    But the problem is that instead of adding the results to the #response div, the page goes to the URL “https://mysite.com/wp-admin/admin-ajax.php”. I assume that it comes from the AJAX request which must have a problem but I can’t find where as the hidden input is there and I tripled checked its value.

    Could you take a look ?

    • Hi Marine,

      Sorry, I have to remove your code – it doesn’t contain any useful information for me 🙃 People copy-paste their own code sometimes even without wrapping it with editor buttons hoping that I will write the code for them.

      So, I understand your problem and let me point you to the correct way. If the page goes to admin-ajax.php, you have to check your browser console, in Chrome – right click on any element and then Inspect, choose the Console tab. It shows you all your JavaScript errors in the code.

      Google Chrome console

      P.S. I don’t remember your emails.

      • Hello :) Thanks for your response ! ( It’s weird, I did wrap the code with the buttons ! )
        I already checked the console, and what’s weird is that I don’t have any errors !
        I figured that it might come from the wp_ajax_ filter, which is why I copied my code thinking it might be relevant !

        In the end, I managed to fix my code. I was submitting the form on input change :

        <input type="checkbox" onchange="this.form.submit()">

        and doing the ajax request also on input change,

        jQuery('input[type=checkbox]').change(function(){ //my function }

        so on submit the page followed the action attribute, as the default behaviour.

        I remove the onchange attribute from the input and it works now.

        Thank you again for this great and useful tutorial :)

      • That’s great, I’m glad you’ve figured it out 🙃 Always welcome!

  • How can I preload the results with no filters?

  • Got that to work, how about pagination? Would this be another jQuery function when click previous or next and add paged #? Thanks for any help you can provide.

    • I have no ready code for this task.

      • hi Misha,
        i am trying to merge the info at premium.wpmudev.org/blog/load-posts-ajax in your code to get exactly the pagination to work, but am quite stuck…

        i’m guessing that the main issue is with data:filter.serialize() whereby wpmu’s uses data:{action:’ajax_pagination’}

        i’d be excellent (and i think it’ll complete your post with an important missing part – as well as becoming my new hero, of course! ;) if you could cast an eye on it and come up with your implementation?

        • MishaAuthor October 20, 2017 at 18:44

          Hi Max,

          Do you mean pagination like on my website? Well, so much people asked me about that, I think soon will be the tutorial 🙃

          • hi Misha,

            yes, that’s the one, something working along the line of paginate_links to have prev/#num#/next buttons to navigate through paged results from your code.

            i do have a navigation script working just fine on the template which loads results the first time, before the filtering. as the site is using WooCommerce already, i’ve reused its styles and the output is similar to yours.

            i think i’ve managed to add the “query_vars” to pass the “paged” value to the serialized data, but not the “page” value that tells the navigation script which page it’s been currently viewed and so it won’t output the prev/#num#/next – it always produces [1][…][0].

            glad to know we’ve grown into an audience pressing you for the code ;) i guess it’s not surprise to you though, as the navigation is the missing cherry on the pie: posts number tends naturally to grow, so there ought to be a way to handle them in anything that deals their listing.

            shame with ajax you have to reinvent the wheel, somehow. it’d have been great if there was WP support for that too (or maybe there’s but i’m too dumb to find it).

  • Hi there. Could you please tell me why my results displaied on example.com/wp-admin/admin-ajax.php page? How I can change it?

  • pagination dont work :(

  • Do you know how I can get each filter to match the search query? I’m trying to include:
    ‘s’ => $search_query
    to each array however I can’t seem to retrieve the search input in the function in functions.php. However I have no problem getting the search input in any other php file. Is this because it hasn’t been loaded in ajax? Any help will be appreciated.

    • MishaAuthor October 10, 2017 at 08:12
      $args = array(
      	's' => $search_query,
      	'orderby' => 'date', // we will sort posts by date
      	'order'	=> $_POST['date'] // ASC или DESC
      );

      And what is in $search_query? I suppose it is empty.

  • Hi!

    Superb tutorial, thank you very much! Was exactly what i needed.

    Is it possible to dynamically change the button text to “show 123 posts” where 123 is the precalculated number of posts that will be shown?

    • MishaAuthor October 10, 2017 at 08:17

      Hi,

      Thank you! :)

      I think you have to send one more request, the simplified one. And use this echo $query->found_posts; to print the number of results.

      But the best practice is to combine these two requests using json response – in this case when you click the button, the posts won’t be loaded because they have been already loaded and will be displayed immediately.

      • Hi!

        Thanks for your reply!

        Can you give me a hint of how to combine the two requests?

        I already managed to call 2 seperate functions – one for the result count and one for the results themselves.

        //Calls the function for HTML results (if( $query->have_posts() ) :?>HTML stuff<?php endif;) on "form submit" and "changes to the sorting"
        jQuery(function($){
        	var filtersetzen = function(){
        		var filter = $('#filter');
        		$.ajax({
        			url:filter.attr('action'),
        			data:filter.serialize()+'&action=fzfilter', // form data
        			type:filter.attr('method'), // POST
        			beforeSend:function(xhr){
        				filter.find('button').text('Wohnmobile laden...'); // changing the button label
        			},
        			success:function(data){
        				filter.find('button').text('Wohnmobile anzeigen'); // changing the button label back
        				$('#response').html(data); // insert data
        				$('html, body').animate({scrollTop: $("#response").offset().top}, 800);
        			}
        		});
        		return false;
        		}
        	$('#filter').submit(filtersetzen);
        	$('#filter_sort').change(filtersetzen);
        });
        //Calls the function for the results count (echo $query->found_posts;)
        jQuery(function($){
        	var filtercount = function(){
        		var filter = $('#filter');
        		$.ajax({
        			url:filter.attr('action'),
        			data:filter.serialize()+'&action=fzfiltercount',
        			type:filter.attr('method'), // POST
        			beforeSend:function(xhr){
        				filter.find('button').prop('disabled',true); // changing the button label
        			},
        			success:function(data){
        				filter.find('button > span').text(data+' ');
        				filter.find('button').prop('disabled',false);
        			}
        		});
        		return false;
        		}
        	$('#filter select').change(filtercount);
        });

        In my understanding there must be a way to just write the HTML results in one variable/array(??) and the result count in a different variable/array(??). Then the ajax function that is called on filter changes returns the result count, and the ajax function that is called by submitting the form returns the HTML results.

      • MishaAuthor October 11, 2017 at 09:33

        Ok, I will try to describe you it in comments, but I think it is better to publish a tutorial about that.

        First of all you have to set dataType: 'json', parameter in you AJAX call.

        Second – in your PHP wp_ajax_ function create an array of two elements, json_encode it and print.

        $return = array( 'html' => $html, 'count' => $count );
        echo json_encode( $return );
        die;

        Third, in your ajax call success() function you can use data.html and data.count properties.

        Something like that 🙃

  • Hello Misha,

    Great code and it’s working perfectly as it is. I wonder whether you could help me to customise it to suit my requirement as with my limited PHP skills I’m pulling my hair to come up with a solution.

    I have this custom field (not using ACF) attached to all my posts (standard WP posts) called ‘Read Time’. Basically I have a function that calculates read time of each posts and enter a number into that field. How can I create a filter to filter post with read times 1-5 minutes, 5-10 minutes and 11+ minutes? I did add this select to form but now struggling how to retrieve and compare the values to suit my query.

     <select name="readtimefilter">
         <option value="5">0-5 Minutes</option>
         <option value="10">6-10 Minutes</option>
         <option value="11">11+ Minutes</option>
       </select>

    Any tips on this is highly appreciated. Many thanks in advance!

    • OK I managed to get that filter working using the following code. Not sure how ‘elegant’ the solution is but it is working.

      if( isset( $_POST['readtimefilter'] ) && $_POST['readtimefilter'] ) {
      	$read_time = (int)$_POST['readtimefilter'];
      	if ($read_time == 5) {
      		$args['meta_query'][] = array(
      			'key' => 'post_read_time',
      			'value' => $read_time+1,
      			'type' => 'numeric',
      			'compare' => '<'
      		);
      	} elseif ($read_time == 10) {
      		$args['meta_query'][] = array(
      			'key' => 'post_read_time',
      			'value' => array( 6, 11 ),
      			'type' => 'numeric',
      			'compare' => 'between'
      		);
      	} else {
      		$args['meta_query'][] = array(
      			'key' => 'post_read_time',
      			'value' => $read_time,
      			'type' => 'numeric',
      			'compare' => '>'
      		);
      	}
      }
  • Great description. I have almost managed to fit my needs this code.
    How can I make an “all” option for the dropdown?
    So if user do not select any (leaves its default state “all”) option from the dropdown all the options considered to be selected.

  • Jiesie Lota October 19, 2017 at 09:30

    Hi Misha,

    Can I ask why I’m having a response of 0? I have all my code exactly the way you coded it but some tweak with my own flow.

    Thanks!

    • MishaAuthor October 20, 2017 at 18:27

      Hi,

      Response 0 could happen if you didn’t connect action variable from your ajax call with appropriate action hook.

      I mean if action=myaction the hooks should be wp_ajax_myaction and wp_ajax_nopriv_myaction.

Leave your question or feedback

phpjsHTMLCSSSQLCode
Please, enter a comment
Please, enter a name
Incorrect email