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)