Terug naar alle berichten
WordPress Filter Posts with Ajax, without page-reload or plugins
Development

WordPress Filter Posts with Ajax, without page-reload or plugins

Geupdate op februari 5, 2024

WordPress Filter without page reload using ajax is not a hard thing to do. Many developers will quickly install another plugin like facetwp or yith to do things like this. But it’s actually pretty simple, as WordPress has default ajax functionalities. For me, as a front-end developer, the hardest part is making the actual WP_Query if we’re extending this method for combined filters WooCommerce.

A more advanced ajax filter for Woocommerce will follow soon! Let’s first start with the basics. A live example of ajax filters in WordPress can be found on one of my live projects: https://eubac.org/publications/

WordPress post filters with ajax

WordPress setup for posts and custom post types

Filtering posts, pages, or custom post types with ajax all works in the same way. I assume you have a basic knowledge of javascript and PHP for the syntax and basic WordPress to know how to make a query. You can filter posts and post types by category, tags, and custom taxonomies.

In this example, I will show you how we can filter our custom post type ‘projects’ by category, based on the category slug. There are many ways to query the posts. I’ll give a quick overview of all possible arguments so you can see which ones work best in your project.

Available WP_Query Arguments

WP Query Category Parameters

  • cat: use category ID – int
  • category_name: use category slug – string
  • category__and: use category IDs – array
  • category__in: use category IDs – array
  • category__not_in: use category IDs – array

WP Query Tag Parameters

  • tag: use tag slug – string
  • tag_id: use tag ID – int
  • tag__and: use tag IDs – array
  • tag__in: use tag IDs – array
  • tag__not__in: use tag IDs – array
  • tag_slug__and: use tag slugs – array
  • tag_slug__in: use tag slugs – array

WP Query Author Parameters

  • author: use author ID – int
  • author_name: use user_nicename, NOT name – string
  • author__in: use author IDs – array
  • author__not_in: use author IDs – array

WordPress Page Setup

To get started, we first need a page that queries all our projects and displays them on that page. We also need to query over our categories, to be able to display all available categories. A live example of the filters can be found on the website of my clients – almost any website with a post-archive needs filters, am I right?

So how do we get started with WordPress filters? Let’s display our available categories first:

<?php $categories = get_categories(); ?>
<ul class="cat-list">
  <li><a class="cat-list_item active" href="#!" data-slug="">All projects</a></li>

  <?php foreach($categories as $category) : ?>
    <li>
      <a class="cat-list_item" href="#!" data-slug="<?= $category->slug; ?>">
        <?= $category->name; ?>
      </a>
    </li>
  <?php endforeach; ?>
</ul>

Notice that our first list item is not within the loop. We’ll use this to remove the filters and display all the posts again. In our anchor-tags you see href=”#!”. We need this because I want to use a-tags for the filters, but anchor tags require a href-tag to be a valid HTML element.

Also, note that I am using data-slug in my list-item links. We’ll need this later in our functions to filter on the category slug.

Querying over WordPress Posts and Custom Post Types

<?php 
  $projects = new WP_Query([
    'post_type' => 'projecten',
    'posts_per_page' => -1,
    'order_by' => 'date',
    'order' => 'desc',
  ]);
?>

<?php if($projects->have_posts()): ?>
  <ul class="project-tiles">
    <?php
      while($projects->have_posts()) : $projects->the_post();
        include('_components/project-list-item.php');
      endwhile;
    ?>
  </ul>
  <?php wp_reset_postdata(); ?>
<?php endif; ?>

This is a basic WP_Query to pull in all our posts from post-type ‘projecten’. Make sure our single list items are coming from a separate PHP file. You can use include like I did, or use get_template_part(). Whatever you prefer.

The include file is just a single <li> item, so you can give it whatever markup you want. I won’t include it in this tutorial, because we’re going straight to the point: filtering

Feel free to reach out to me or leave a comment in case you want to see a full setup, that also explains the project-list-item.php file. For testing your code, you can add the_title(); in this file for now.

The WordPress filter with Javascript!

For me as a front-ender, obviously the most fun part! We’ll be running a filter function when we click on a category link we’ve made before. We’ll be doing 2 things:

  1. Make the clicked category the new active filter
  2. Display only the projects that belong to that category

EASY. But let’s not celebrate too early. This is only the JS part, we do need some custom PHP code later on in order to make this stuff work.

$('.cat-list_item').on('click', function() {
  $('.cat-list_item').removeClass('active');
  $(this).addClass('active');

  $.ajax({
    type: 'POST',
    url: '/wp-admin/admin-ajax.php',
    dataType: 'html',
    data: {
      action: 'filter_projects',
      category: $(this).data('slug'),
    },
    success: function(res) {
      $('.project-tiles').html(res);
    }
  })
});

This doesn’t look too complicated, does it? We click our category links and create a new $.ajax call. We will be posting our data to a custom function called ‘filter_projects’. We also need to post the slug we want to filter and we do that by passing our filter to our data ‘category’.

The AJAX URL is the default in WordPress: /wp-admin/admin-ajax.php

NOTE: If the relative URL /wp-admin/admin-ajax.php is not working for you and returns a 404 instead, try using the absolute URL

The PHP WordPress Filter, the horror!

Just kidding. Ajax queries in WordPress with PHP all sounds like a nightmare, but it’s all fun and games! Because we are using the default /wp-admin/admin-ajax.php URL, we can write this function somewhere in the functions.php file:

function filter_projects() {
  $catSlug = $_POST['category'];

  $ajaxposts = new WP_Query([
    'post_type' => 'projecten',
    'posts_per_page' => -1,
    'category_name' => $catSlug,
    'orderby' => 'menu_order', 
    'order' => 'desc',
  ]);
  $response = '';

  if($ajaxposts->have_posts()) {
    while($ajaxposts->have_posts()) : $ajaxposts->the_post();
      $response .= get_template_part('templates/_components/project-list-item');
    endwhile;
  } else {
    $response = 'empty';
  }

  echo $response;
  exit;
}
add_action('wp_ajax_filter_projects', 'filter_projects');
add_action('wp_ajax_nopriv_filter_projects', 'filter_projects');

When hitting the ‘filter_projects’ function, we’ll be checking if we did POST our category-slug. If it’s an empty string, the query won’t filter and it will return all our posts. (we use it when we click the filter “show all projects”)

Next, we run our WP_Query again, but this time with an extra parameter for the category name. Ironically enough, it’s using the SLUG and not the name of the category. Afterward, we just loop over query results and add the template part as a string to our $results.

Don’t forget to register our function, so WordPress knows our ‘filter_projects’ exist. Otherwise, we’ll receive an error when we try to make our ajax call.

Filter Dynamic custom post types with ajax

If you are using multiple post-types that are using the same taxonomies, we can dynamically pass our post_type to our ajax filter function as well. This way we can reuse our previous setup for all posts types on our page.

To be able to do this, we take our PHP list setup from the example above and add a new data attribute ‘data-type’ to also add the post type:

The following code snippets display only the parts that changed from the code examples from before. If something is not working correctly, make sure to use the code from above to set up all the triggers and functions.

<li>
  <a class="cat-list_item" href="#!" data-slug="<?= $category->slug; ?>" data-type="projecten">
    <?= $category->name; ?>
  </a>
</li>

Our data-type can be dynamic as well of course if you’re using the same filter component on all pages. I’ve kept it static in my example for simplicity. Now you can create multiple lists for each post-type and change the data-type attribute to the post type you want to filter.

In our javascript function, we also add an additional parameter in our Ajax filter function:

$.ajax({
  type: 'POST',
  url: '/wp-admin/admin-ajax.php',
  dataType: 'html',
  data: {
    action: 'filter_projects',
    category: $(this).data('slug'),
    type: $(this).data('type'),
  },
  success: function(res) {
    $('.project-tiles').html(res);
  }
});

And at last, we need to make sure our PHP code uses our dynamic post type argument to filter our post types. Update the PHP function code in functions.php to look like this:

function filter_projects() {
  $postType = $_POST['type'];
  $catSlug = $_POST['category'];

  $ajaxposts = new WP_Query([
    'post_type' => $postType,
    'posts_per_page' => -1,
    'category_name' => $catSlug,
    'orderby' => 'menu_order', 
    'order' => 'desc',
  ]);
  $response = '';

  if($ajaxposts->have_posts()) {
    while($ajaxposts->have_posts()) : $ajaxposts->the_post();
      $response .= get_template_part('templates/_components/project-list-item');
    endwhile;
  } else {
    $response = 'empty';
  }

  echo $response;
  exit;
}
add_action('wp_ajax_filter_projects', 'filter_projects');
add_action('wp_ajax_nopriv_filter_projects', 'filter_projects');

Filter custom taxonomies in WordPress with ajax

When filtering your posts using a custom taxonomy instead of the regular tags or categories, we need to edit our query a little bit. The way we can filter on custom taxonomies is slightly different. We need to add a tax_query to our normal query.

a tax_query is an array, existing of arrays. This actually brings us fluently to creating an ajax filter that supports multiple filters at once. But let’s just start with displaying all posts for the selected custom taxonomy.

Our query will look like this:

$projects = new WP_Query([
   'post_type' => 'projecten',
   'posts_per_page' => -1,
   'tax_query' => [
      [
         'taxonomy' => 'product_cat',
         'field'    => 'term_id',
         'terms'    => $termIds, // example of $termIds = [4,5]
         'operator' => 'IN'
      ],
   ]
]);

Note that our taxonomy query is an array of arrays. $termIds is also an array, with all termIds I want to filter on. Thanks to the taxonomy query being its own argument, we can combine taxonomy filters together with other category or tag filters.

Combining multiple WordPress filters with Ajax

combining wordpress ajax filters

Let’s combine multiple filters at the same time! The picture shows example of how it works and will look. It’s somewhat similar to creating the regular filters we did above, but can quickly become a mess. So I’m going to seperate our ajax filters in a seperate filters.js file. The PHP part will still be in functions.php but you can place it in your own WordPress plugin folder if you’d like. I’m not going to cover creating WordPress Plugins, so my solution will be straight in functions.php 🙂

To get started on a filter like above, I created a custom sidebar with all my different filters in it:

<div class="filter--sidebar">
  <input type="hidden" id="filters-category" />
  <input type="hidden" id="filters-creators" />

  <?php 
    // or use include_once('parts/filter-pricerange.php');
    get_template_part('parts/filter','pricerange');

    get_template_part('parts/filter','categories');
  
    get_template_part('parts/filter','creators');
  ?>
</div>

Note the two hidden fields for our categories and creators. We will fill those up with javascript and then post those values to our ajax query. More on this later!

You can add every filter-part you want like this. Because I covered the categories above, I will cover creators first. This is a relationship field with the ACF Plugin.

Our parts/filter-creators.php file shows a list of all our creators/brands. This makes it possible for users to filter on the brand they like. This file simply loops over all our brands and shows them in a list:

<?php 
$get_creators = new WP_Query([
  'post_type' => 'creators',
  'posts_per_page' => -1,
]);

if ($get_creators->have_posts()): ?>
  <h4 class="filter-title"><?= _e('Creators','weichie'); ?></h4>
  <ul class="list-filters">
    <?php while ($get_creators->have_posts()):$get_creators->the_post(); ?>
      
      <li>
        <a href="javascript:;" class="filter-link" data-type="creators" data-id="<?= get_the_ID(); ?>">
          <?= esc_html(get_the_title()); ?>
          <span class="remove"><i class="fas fa-times"></i></span>
        </a>
      </li>

    <?php endwhile; ?>
  </ul>
  <?php wp_reset_postdata(); ?>
<?php endif; ?>

So far so good? I don’t think anything special is going on in here. We create a new WP_Query for our ‘authors’ post type and loop over them. You do need to hide the span. remove the element with CSS. This one we will show only when someone has selected that filter.

Creators ACF Field filter

Maybe it’s also interesting to show you how I’ve linked our creators to the products. I’m using the ACF Pro Plugin to create the fields and place them as a relationship field onto our products. ‘Creators’ is a custom post type on its own.

acf relationship filter

For each product in our store, I can select one or more brands that are ‘related to’ this product. So it would be nice to have if users could now filter our products based on this brand.

Next up: our filter.js file! You don’t have to, but if I like to keep things clear. The filter-file will contain all our js-related filter stuff. So if something breaks or we need to change or add something, it’s always easy to find.

You remember our hidden fields in our PHP filters? Now is the time we’re going to use them. When clicking on a creator, we’ll be putting the IDs of our selected brands in the input field.

filters.js :

$('.filter-link').on('click', function(e) {
    e.preventDefault();
    $(this).toggleClass('activeFilter');

    editFilterInputs($('#filters-' + $(this).data('type')), $(this).data('id'));
    filterProducts();
  });

So for each filter-link we’re clicking on, we’ll toggle our ‘activeFilter’ class (to show/hide the remove icon on the filter), we’ll edit the values in the hidden input fields and we’ll be calling our filterProducts() function to actually filter our products.

To place our values in our hidden fields, we need to make sure we’re not adding a filter twice. Clicking on a filter will add the ID to our hidden field. Clicking on it again will remove the ID from the hidden input field:

function editFilterInputs(inputField, value) {
  const currentFilters = inputField.val().split(',');
  const newFilter = value.toString();

  if (currentFilters.includes(newFilter)) {
    const i = currentFilters.indexOf(newFilter);
    currentFilters.splice(i, 1);
    inputField.val(currentFilters);
  } else {
    inputField.val(inputField.val() + ',' + newFilter);
  }
}

The editFilterInputs function takes an input field – the one where we want to place our filters. Our creator IDs need to go into the creator hidden input field of course. Then we check if the ID already exists in the input field or not. If so, we’ll remove the ID, otherwise we’ll add it.

Our input-field has a value of string. I’m converting the current value in our input field to an array to make it easier to check if the value already exists. If so, remove it, otherwise add it.

Now our main filterProducts() function:

function filterProducts() {
  const catIds = $('#filters-category').val().split(',');
  const creatorIds = $('#filters-creators').val().split(',');
  
  $.ajax({
    type: 'POST',
    url: '/wp-admin/admin-ajax.php',
    dataType: 'json',
    data: {
      action: 'filter_products',
      catIds,
      creatorIds,
    },
    success: function(res) {
      $('#result-count').html(res.total);
      $('#main-product-list').html(res.html);
    },
    error: function(err) {
      console.error(err);
    }
  })
}

The JS part actually looks a lot like a single category/tag/taxonomy filter. But this one will be used for all our filters in the future. In this example, it will work for both creators and categories. You can add all other filters in exactly the same way.

Please note that I am again converting the hidden input fields to an array before posting it to our PHP script to actually make the query and return our response.

Multiple Ajax Filters – PHP part

The PHP part is a bit different than usual. I don’t know if I am explaining any of this clear to you, but please bear with me. We’re almost there and I hope you can make it to work on your project as well!

Getting started in the PHP file, we’ll create our filter_products() filter, receive our $_POST value from the creators and categories and just start with our basic query (without filters) for now.

function filter_products() {
	$catIds = $_POST['catIds'];
	$creatorIds = $_POST['creatorIds'];

	$args = [
		'post_type' => 'product',
		'posts_per_page' => -1,
		'post_status'  => 'publish',
		'orderby'        => 'date',
                'order'          => 'desc',
	];
}
add_action('wp_ajax_filter_products', 'filter_products');
add_action('wp_ajax_nopriv_filter_products', 'filter_products');

Is this still going fine? We get the POST values and create an $args array with our basic query values. We are using a separate $args variable to make it easier to add our multiple filters to the query array.

Next, we’ll add our product categories to the $args array. This one uses the WordPress default ‘product_cat’ taxonomy to filter on a product category: add this part right below our previous $args. In the same filter_projects() function.

// Product Category
if (count($catIds) > 1) {
	$args['tax_query'][] = [
		'taxonomy'      => 'product_cat',
		'field'		=> 'term_id',
		'terms'         => $catIds,
		'operator'      => 'IN'
	];
}

Also, nothing too crazy I think? It takes our $catIds from our $_POST from before. By using $args[’tax_query’][] we add this array to the already existing $args array from before. We’re not overwriting anything, we’re adding a ’tax_query’ array to our previous array.

We’ll be adding our creators’ filter to the array the same way. Only is Creators a Custom Post Type and not a Taxonomy. So instead of using tax_query we need to use meta_query for this field:

// Product creator - Every creator needs a seperate meta_query
if(count($creatorIds) > 1) {
	$args['meta_query']['relation'] = 'OR';

	foreach($creatorIds as $creator) :
		if ($creator != '') :
			$args['meta_query'][] = [
				'key' => 'product_creator',
				'value' => '"' . $creator . '"',
				'compare' => 'LIKE',
			];
		endif;
	endforeach;
}

Each creator needs its own [‘meta_query’] array. Only if there is a creator selected, we’ll be adding this filter to our $args.

Now our $args filter has the default query for our products, a tax_query for our categories, and a meta_query for our ACF Relationship queries.

Now it’s time for the main query. This one is exactly the same as the singular filters from above, but I changed the response a little bit so we receive our query total (to see how many products are in the filter) and then our $html result.

Place this code in the same filter_products() function, right below our previous $args functions for categories and custom post types:

if ( $ajaxproducts->have_posts() ) {
	ob_start();
	while ( $ajaxproducts->have_posts() ) : $ajaxproducts->the_post();
		$response .= wc_get_template_part( 'content', 'product-dibbz' );
	endwhile;
	$output = ob_get_contents();
	ob_end_clean();
} else {
	echo __( 'No products found' );
}
	
$result = [
	'total' => $counter,
	'html' => $output,
];

echo json_encode($result);
wp_reset_postdata();
exit;

WordPress Ajax Filter Summary

This is all we need to make ajax filters work in WordPress. Let me know if the ajax filters were easier than you thought or if you’re still having difficulties.

A more advanced, dynamic ajax filter for Woocommerce will follow somewhere next month. I still have some difficulties with filtering e-commerce products with ajax to make it super fast. The post will be ready when I’m done struggling. 😉

Edit 2021: I get a lot of reactions about doing a tutorial with ajax filters on default WordPress posts (the WordPress index page and category.php) – but then with Ajax. I want to write a tutorial on that topic as well, but it actually works the same way as this article. Except that WordPress handles the first post-query for us by default.

Please let me know if this tutorial escalated too quickly and you would prefer a more basic guide using the WordPress archive pages with ajax post filters.

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *