Simple WP_Query Ajax

Simple WP_Query Ajax

WP_Query is a powerful class that, amongst other things, allows you to define the query at any point on a page, then list all posts relevant to the provided arguments. In this article we’re taking it one step further and demonstrating how to create an ajax-powered, taxonomy-filterable WP_Query loop, with pagination and search functionality.

Post Type & Taxonomy Setup

You could use the default post and category combination (or any other custom post type and taxonomy for that matter), but we’ll make use of the bite-size snippets provided by the WP Codex to create a “Book” post type with the “Genre” taxonomy bolted on:

 //Register books post type
 function codex_custom_init() {
 $args = array(
 'public' => true,
 'label' => 'Books'
 );
 register_post_type( 'book', $args );
 }
 add_action( 'init', 'codex_custom_init' );
//Register book genre taxonomy
add_action( 'init', 'create_book_tax' );

function create_book_tax() {
	register_taxonomy(
		'genre',
		'book',
		array(
			'label' => __( 'Genre' ),
			'rewrite' => array( 'slug' => 'genre' ),
			'hierarchical' => true
		)
	);
}

Once you’ve added both of these to your theme’s functions.php file, head over to the WordPress back-end and add a handful of test posts and genres, then assign different posts to one or more genres – now your posts are ready for filtering!

Template Page Setup

For this example we’ll create a custom template file called “page-books.php”; just put this directly on the root of your theme and let WordPress’ template hierarchy handle the template theming. Make sure you add a “Books” page in WordPress as well.

<?php
/*
 * Custom Books Page Template
 */

get_header();
?>

<section id="primary" class="content-area">
	<div id="content" class="site-content" role="main">
	<?php
    if( have_posts() ):
        while( have_posts() ): the_post();
            get_template_part('content');
        endwhile;
    endif;
    ?>
	</div>
</section>

<?php get_footer(); ?>

Filter Markup

Before we get started with the actual post loop we need to create the filters for our genres. In the interest of simplicity all of the functions will be contained in functions.php of your current theme then called from the template file (page-books.php).

The below function simply stores all of your genre terms using get_terms(), then goes on to loop through the array, adding each term name to a list along with a child input thats value is equal to the term ID:

//Get Genre Filters
function get_genre_filters()
{
	$terms = get_terms('genre');
	$filters_html = false;

	if( $terms ):
		$filters_html = '<ul>';

		foreach( $terms as $term )
		{
			$term_id = $term->term_id;
			$term_name = $term->name;

			$filters_html .= '<li class="term_id_'.$term_id.'">'.$term_name.'<input type="checkbox" name="filter_genre[]" value="'.$term_id.'"></li>';
		}
		$filters_html .= '<li class="clear-all">Clear All</li>';
		$filters_html .= '</ul>';

		return $filters_html;
	endif;
}

You can then call this function and style it to your choosing on the page template. Make sure to echo the function as it only stores the output, rather than echoing it:

<div id="genre-filter">
    <?php echo get_genre_filters(); ?>
</div>

Since we are in our template file, we may as well go ahead and add the search form and an empty div with the id “genre-results”, ready to house our book post data:

<div class="entry-content">
    <form id="genre-search">
        <input type="text" class="text-search" placeholder="Search books..." />
        <input type="submit" value="Search" id="submit-search" />
    </form>
    <div id="genre-filter">
        <?php echo get_genre_filters(); ?>
    </div>
    <div id="genre-results"></div>
</div>

Enqueue Scripts

Now that all of the markup is in place, we can focus on sending through the data using WordPress’ admin ajax – just throw the following lines into the bottom of functions.php and create the “/js/genre.js” javascript file in your theme root:

//Enqueue Ajax Scripts
function enqueue_genre_ajax_scripts() {
    wp_register_script( 'genre-ajax-js', get_bloginfo('template_url') . '/js/genre.js', array( 'jquery' ), '', true );
    wp_localize_script( 'genre-ajax-js', 'ajax_genre_params', array( 'ajax_url' => admin_url( 'admin-ajax.php' ) ) );
    wp_enqueue_script( 'genre-ajax-js' );
}
add_action('wp_enqueue_scripts', 'enqueue_genre_ajax_scripts');

Notice we are setting a jQuery dependancy on the wp_register_script, this should make sure we correctly include the latest version of jQuery included with WordPress without sourcing duplicate jQuery libraries. We also use wp_localize_script to localise the admin ajax url so that we can use it inside “/js/genre.js” as you’ll see later on.

Genre.js

Our “genre.js” contains all of the scripts that we’ll use to trigger requests, find the relevant values and process the return data. There are a handful of functions that handle each event, so I’ve commented the scripts to explain whats happening:

//Genre Ajax Filtering
jQuery(function($)
{
	//Load posts on document ready
	genre_get_posts();

	//If list item is clicked, trigger input change and add css class
	$('#genre-filter li').live('click', function(){
		var input = $(this).find('input');

                //Check if clear all was clicked
		if ( $(this).attr('class') == 'clear-all' )
		{
			$('#genre-filter li').removeClass('selected').find('input').prop('checked',false); //Clear settings
			genre_get_posts(); //Load Posts
		}
		else if (input.is(':checked'))
		{
			input.prop('checked', false);
			$(this).removeClass('selected');
		} else {
			input.prop('checked', true);
			$(this).addClass('selected');
		}

		input.trigger("change");
	});

	//If input is changed, load posts
	$('#genre-filter input').live('change', function(){
		genre_get_posts(); //Load Posts
	});

	//Find Selected Genres
	function getSelectedGenres()
	{
		var genres = []; //Setup empty array

		$("#genre-filter li input:checked").each(function() {
			var val = $(this).val();
			genres.push(val); //Push value onto array
		});		

		return genres; //Return all of the selected genres in an array
	}

	//Fire ajax request when typing in search
	$('#genre-search input.text-search').live('keyup', function(e){
		if( e.keyCode == 27 )
		{
			$(this).val(''); //If 'escape' was pressed, clear value
		}

		genre_get_posts(); //Load Posts
	});

	$('#submit-search').live('click', function(e){
		e.preventDefault();
		genre_get_posts(); //Load Posts
	});

	//Get Search Form Values
	function getSearchValue()
	{
		var searchValue = $('#genre-search input.text-search').val(); //Get search form text input value
		return searchValue;
	}

	//If pagination is clicked, load correct posts
	$('.genre-filter-navigation a').live('click', function(e){
		e.preventDefault();

		var url = $(this).attr('href'); //Grab the URL destination as a string
		var paged = url.split('&paged='); //Split the string at the occurance of &paged=

		genre_get_posts(paged[1]); //Load Posts (feed in paged value)
	});

	//Main ajax function
	function genre_get_posts(paged)
	{
		var paged_value = paged; //Store the paged value if it's being sent through when the function is called
		var ajax_url = ajax_genre_params.ajax_url; //Get ajax url (added through wp_localize_script)

		$.ajax({
			type: 'GET',
			url: ajax_url,
			data: {
				action: 'genre_filter',
				genres: getSelectedGenres, //Get array of values from previous function
				search: getSearchValue(), //Retrieve search value using function
				paged: paged_value //If paged value is being sent through with function call, store here
			},
			beforeSend: function ()
			{
				//You could show a loader here
			},
			success: function(data)
			{
				//Hide loader here
				$('#genre-results').html(data);
			},
			error: function()
			{
                                //If an ajax error has occured, do something here...
				$("#genre-results").html('<p>There has been an error</p>');
			}
		});
	}

});

Genre Ajax Action

With our ajax scripts ready and able to send the correct category, pagination and search values, we need a PHP function to process all of the information for the WP_Query class to harness – that’s where our action comes into play. Lets take a quick look at all of the components.

First off we setup our actions, and their callback functions. We have to define the “nopriv_” action as well for users who aren’t logged in, this wouldn’t be necessary when developing back-end admin ajax functionality (as the user will be logged in).

//Add Ajax Actions
add_action('wp_ajax_genre_filter', 'ajax_genre_filter');
add_action('wp_ajax_nopriv_genre_filter', 'ajax_genre_filter');

After that, we create the function “ajax_genre_filter”, which should correspond with the callback above. We then save all of the values in a “$query_data” variable for us to dissect.

//Construct Loop & Results
function ajax_genre_filter()
{
    $query_data = $_GET;

Next, check if any categories are selected – if not, just set as false:

$genre_terms = ($query_data['genres']) ? explode(',',$query_data['genres']) : false;

After this we make sure the terms exist before setting up a tax_query to handle the genre taxonomy filter:

$tax_query = ($genre_terms) ? array( array(
    'taxonomy' => 'genre',
    'field' => 'id',
    'terms' => $genre_terms
) ) : false;

We do the same check that we did on the genres, but this time for the search value:

$search_value = ($query_data['search']) ? $query_data['search'] : false;

Our final check is for the paged variable, something which is only fed through by clicking on the pagination links. If none were clicked, we simply use a 1 (for page 1):

$paged = (isset($query_data['paged']) ) ? intval($query_data['paged']) : 1;

Now we have all our checks in place, we can string this all together in an arguments variable for the WP_Query to digest:

$book_args = array(
    'post_type' => 'book',
    's' => $search_value,
    'posts_per_page' => 2,
    'tax_query' => $tax_query,
    'paged' => $paged
);

Our loop settings are in place, all that’s left to do now is add the standard WP_Query loop and pagination links, followed by a “die()” to properly close the ajax callback function:

$book_loop = new WP_Query($book_args);

	if( $book_loop->have_posts() ):
		while( $book_loop->have_posts() ): $book_loop->the_post();
			get_template_part('content');
		endwhile;

		echo '<div class="genre-filter-navigation">';
		$big = 999999999;
		echo paginate_links( array(
			'base' => str_replace( $big, '%#%', esc_url( get_pagenum_link( $big ) ) ),
			'format' => '?paged=%#%',
			'current' => max( 1, $paged ),
			'total' => $book_loop->max_num_pages
		) );
		echo '</div>';
	else:
		get_template_part('content-none');
	endif;
	wp_reset_postdata();

	die();
}

And there you have it – a relatively simple and effective way to paginate, search and filter a WP_Query using the power of ajax!

Simple WP_Query Ajax

Files

Attached below are the three files we discussed in the article (functions.php, page-blog.php, genre.js) that you can copy and paste into your own theme to get started.

Simple WP_Query Ajax (5KB)

  • Taruckus

    This works great! thank you.

    I couldn’t figure out using get_taxonomies() how to loop for multiple taxonomies and its terms within each, so I just named multiple variables with a taxonomy each using get_terms(), then repeated each loop in functions.php (there were 4 to go through). Didn’t win the beauty pageant, but it’s effective.

    Perhaps I could have collected all 4 taxonomies as an array in 1 get_terms(), then used values term_taxonomy_id or taxonomy to separate the terms out.

  • Hue

    Hi, I am trying to get this to work from the zip file you kindly provided, I have it all installed correctly and seeing posts and filter on page, however it doesn’t seem to be filtering. I am getting an js error: ‘Uncaught TypeError: undefined is not a function’ on this line: ‘$(‘#genre-filter li’).live(‘click’, function(){‘ any ideas?

    • Shane Welland

      Hi Hue, is your jQuery being loaded in correctly? Also you could change “$” to “jQuery” as a quick test. I’m using “jQuery(function($) {” as my document ready statement, perhaps you are not?

      • Matt Hull

        I worked out the issue, apparently .live() was removed in Jquery 1.9. On another note, I want to adjust this to just show and filter the sub categories of one category e.g Genre Books children. Is this possible?

  • Uncle A

    Hi, I am trying something very similar to what you are showing here. The problem is that the WP_Query is always adding 1=2 in the where clause.
    The code in question is called by our index.php with a get_template_part(…). I am also calling the same template from my ajax callback function using get_template_part () and WP_Query is adding 1=2 to the where clause.

    Any suggestions of what I could be doing wrong?

  • steve

    This is great, works like a charm.
    I tried (unsuccessfully) to modify it so it could handle ‘meta_key’ and ‘meta_value’. If you ever feel like a follow up post including custom fields I for one would vouch for it.
    Thanks for this though.

    • Shane Welland

      Good, I’m glad to hear! I’ll revisit this with a meta_query equivalent/addition in the next week or so – it should be a fairly similar process and the results all run off of WP_Query, just this time using a meta_query

  • Pete Sorensen

    Thanks for the tutorial. Quick question: Shouldn’t the ‘action’ attribute in the .ajax call be `ajax_genre_filter` not `genre_filter`?

  • JJ

    Make a demo!

  • erite

    Hello,
    Running into some issues with this. When I click a page number in the pagination nav, it takes me to a templateless page with the results of the query function. Ajax functions normally on that page (the url is the ajax url with the query params appended). Also, when I click a checkbox, it acts as though it’s reloading the info, but it seems the checkbox filter doesn’t get applied and the same results are spit out. I have wordpress in a subdirectory, that’s the only thing I can think of that might be causing an issue (no other plugins and a pretty much empty theme on WP4.1.1). Any ideas?

    • lightsandswirls

      I’m getting this same error with the pagination links going to template-less pages (though still outputting the proper posts for that page of the query). Filtering otherwise works like a charm. If anyone has a solution to the pagination issue, I’d be really grateful because with hundreds of posts, I’ll definitely need to paginate!

      Also, erite, my wp install is in the root, so I’m guessing that’s not your issue. For your checkbox clicking not producing any results, try clicking on just the text instead – I noticed that with my implementation as well. I guess .on(‘click’) doesn’t register for clicking on a checkbox input, even if it’s inside the targeted container (but don’t quote me on that). I used css to hide the checkboxes and then styled the li text to look clickable to make it obvious for the user.

      • erite

        Yea WP being in a subdirectory wasn’t the issue. In my case, I had the functions in a php file that required a namespace and I wasn’t passing it with add_action(). The checkbox issue mentioned was a result of the ajax call returning 0 (because of the namespace issue), but I did notice that they worked only when clicking the text as well. You can fix that by changing the selector, or with CSS as you did. I actually used chained select boxes instead because my project called for it. One select deals with taxonomy and the other with custom fields. Also cleaned up the code with jslint and updated .live() to .on(). I’d be happy to share the code although I’m not using pagination for this project either, should be simple to implement though.

        • lightsandswirls

          Thanks, I’d love to take a look if you don’t mind. I’m still at a loss as to why my pagination isn’t working. Knowing me, it’s some tiny little detail that I’m missing 😀

          • erite

            i put the code in a gist but the comment with the link is awaiting moderation. let me know if there’s some other way to send it to you

    • erite

      Thanks for the great tutorial btw :) got it solved

      • Nicolas Florth

        Hi! Can you tell me please how you solved the pagination link? Thanks!

        • Billy

          I figured it out! Make sure that your selector is targeting the right thing.

          For me, I accidentally renamed my pagination div to better match the rest of the site. If you don’t rename the selector in the JS file as well, poof, pagination breaks.

  • PS

    This is a great tutorial and works great. Does anyone have any idea how I could update the count for each term in the filter?
    Any help would be great.
    Thanks.

  • Nelly Gretsch

    What do i need to change / alter to use just normal post types and call this from a custom page – not default page-books.php?

    Thanks!!!!

  • Dan

    Firstly, awesome tutorial – works straight out of the box. One question – how would you show all terms even if empty and echo out ‘Sorry no posts’ for that term?

    Thanks!

  • wideseo media

    How to display the taxonomy terms in hierarchical?