An extensive snippet that allows for various useful functionalities.
This is building on my basic AJAX integration post. It is an extensive snippet, but will include a bunch of features I find myself using over and over again. Those features include:
Please do remove whatever you do not need.
<div class="search-bar">
<div class="search-bar__categories">
<button class="cat-tag cat-tag--active" data-category="">All</button>
<?php $categories = get_categories();
foreach( $categories as $category ) : ?>
<button class="cat-tag" data-category="<?= esc_attr($category->slug); ?>"><?= esc_html($category->cat_name); ?></button>
<?php endforeach; ?>
</div>
<div class="seach-bar__search">
<input class="seach-bar__input" type="text" name="s" id="search-input" value="<?php the_search_query(); ?>" placeholder="Search for a snippet" required autocomplete="off">
<button class="search-bar__clear" title="Clear search input" id="clear">
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" version="1.1" viewBox="0 0 22.8 22.8">
<defs>
<style>
.icon-close{fill:none;stroke:#fff;stroke-linecap:round;stroke-width:2px}
</style>
</defs>
<g id="icon-close">
<path id="Line_322" d="m1.4 1.4 20 20" class="icon-close" data-name="Line 322"/>
<path id="Line_323" d="m1.4 21.4 20-20" class="icon-close" data-name="Line 323"/>
</g>
</svg>
</button>
</div>
</div>
<div class="filter-bar">
<div class="filter__sort">
<select id="filter-sort" class="filter__dropdown">
<option value="newest">Sort by newest</option>
<option value="recent">Sort by recently updated</option>
<option value="oldest">Sort by oldest</option>
<option value="popular">Sort by most popular</option>
</select>
</div>
</div>
export default () => {
const searchInput = jQuery('#search-input');
const container = jQuery('#ajax-post-results');
const top = jQuery('#top');
const pagination = jQuery('.pagination');
const prevPageButton = jQuery('#prev-page');
const nextPageButton = jQuery('#next-page');
const currentPageSpan = jQuery('.pagination__current-page');
const totalPagesSpan = jQuery('.pagination__total-pages');
const sortSelect = jQuery('#filter-sort');
const categoryButtons = jQuery('.cat-tag');
const clear = jQuery('#clear');
let selectedCategory = '';
let currentPage = 1;
let totalPages = 1;
// Helper function to get a URL parameter
const getURLParam = (param, defaultValue = '') => {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(param) || defaultValue;
};
// Helper function to update the URL without reloading
const updateURL = (param, value) => {
const url = new URL(window.location);
url.searchParams.set(param, value);
window.history.pushState({}, '', url);
};
// Function to update pagination UI
const updatePaginationUI = () => {
if( totalPages > 1 ) {
pagination.css('display', 'flex');
} else {
pagination.css('display', 'none');
}
currentPageSpan.text(currentPage);
totalPagesSpan.text(totalPages);
prevPageButton.prop('disabled', currentPage === 1);
nextPageButton.prop('disabled', currentPage >= totalPages);
};
// Function to scroll the page to the top on pagination change
const scrollToTop = () => {
jQuery('html, body').animate({
scrollTop: top.offset().top - 50,
}, 500);
};
// Function to filter posts via AJAX
const filterPosts = () => {
jQuery.ajax({
url: ajaxurl.url,
type: 'POST',
data: {
action: 'filter_posts',
search: searchInput.val(),
page: currentPage,
category: selectedCategory,
sort: sortSelect.val(),
},
beforeSend: () => {
container.html('<div class="spinner"><div></div><div></div><div></div><div></div></div>');
},
success: (response) => {
container.html(response.html);
totalPages = response.max_pages;
updatePaginationUI();
},
});
};
// Function to initialize values from the URL
const initializeFiltersFromURL = () => {
selectedCategory = getURLParam('category');
searchInput.val(getURLParam('search'));
sortSelect.val(getURLParam('sort', 'newest'));
// Highlight the correct category button
categoryButtons.removeClass('cat-tag--active');
jQuery(`.cat-tag[data-category="${selectedCategory}"]`).addClass('cat-tag--active');
};
// Event: Category Click
categoryButtons.on('click', function (e) {
e.preventDefault();
selectedCategory = jQuery(this).data('category') || '';
categoryButtons.removeClass('cat-tag--active');
jQuery(this).addClass('cat-tag--active');
currentPage = 1;
updateURL('category', selectedCategory);
filterPosts();
});
// Event: Pagination Buttons
prevPageButton.on('click', () => {
if (currentPage > 1) {
currentPage--;
filterPosts();
scrollToTop();
}
});
nextPageButton.on('click', () => {
if (currentPage < totalPages) {
currentPage++;
filterPosts();
scrollToTop();
}
});
// Event: Search Input with Debounce
searchInput.on('keyup', () => {
clearTimeout(window.searchTimeout);
window.searchTimeout = setTimeout(() => {
currentPage = 1;
updateURL('search', searchInput.val());
filterPosts();
}, 300);
// Show clear button if input has value
if( searchInput.val() ) {
clear.css('display', 'block');
} else {
clear.css('display', 'none');
}
});
// Event: Clear search input
clear.on('click', () => {
searchInput.val('');
clear.css('display', 'none');
currentPage = 1;
updateURL('search', searchInput.val());
filterPosts();
});
// Event: Sort Select Change
sortSelect.on('change', () => {
updateURL('sort', sortSelect.val());
filterPosts();
});
// Initialize filters and trigger initial post load
initializeFiltersFromURL();
filterPosts();
};
And make sure to import this code in your main.js file.
<?php
// Handle filtering for projects
add_action('wp_ajax_filter_posts', 'filter_posts_function');
add_action('wp_ajax_nopriv_filter_posts', 'filter_posts_function');
function filter_posts_function() {
$search_query = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
$category = isset($_POST['category']) ? sanitize_text_field($_POST['category']) : '';
$sort_option = isset($_POST['sort']) ? sanitize_text_field($_POST['sort']) : 'newest';
$page = isset($_POST['page']) ? intval($_POST['page']) : 1;
$args = [
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => get_option('posts_per_page'),
'paged' => $page,
's' => $search_query,
];
// Add category filter only if a specific category is selected
if (!empty($category)) {
$args['category_name'] = $category;
}
// Add sorting options
if ($sort_option === 'newest') {
$args['orderby'] = [
'date' => 'DESC'
];
} elseif ($sort_option === 'recent') {
$args['orderby'] = [
'modified' => 'DESC'
];
} elseif ($sort_option === 'oldest') {
$args['orderby'] = [
'date' => 'ASC'
];
} elseif ($sort_option === 'popular') {
$args['orderby'] = [
'post_views' => 'ASC'
];
}
$query = new WP_Query($args);
$total_posts_query = new WP_Query(array_merge($args, ['posts_per_page' => -1]));
$total_posts = $total_posts_query->found_posts;
ob_start();
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
echo '<article class="card">';
get_template_part('inc/card-content');
echo '</article>';
}
} else {
if ($page === 1) {
echo '<p class="loop-message">Sorry, no results were found.</p>';
}
}
$html = ob_get_clean();
wp_reset_postdata();
// Output JSON response
wp_send_json([
'html' => $html,
'max_pages' => $query->max_num_pages,
'total_posts' => $total_posts,
]);
wp_die(); // Required to terminate immediately and return a proper response
}
And import this code in your functions.php file: get_template_part('functions/ajax-filters-posts.php');
function scripts() {
// AJAX URL
wp_localize_script('js', 'ajaxurl', array(
'url' => admin_url('admin-ajax.php')
));
}
Include your filter.php file wherever you need it and create the container div where your posts will be injected. For me, it was in my projects.php template.
<?php get_template_part('inc/filter'); ?>
<div class="articles__loop" id="ajax-post-results" data-max-pages="<?= $wp_query->max_num_pages; ?>">
<?php /** Filtered posts will be injected here */ ?>
</div><!-- END articles__loop -->
<button id="load-more-btn" class="load-more-button">Load more projects</button>