Custom Ajax Search filter for WordPress Search
I want to give a complete breakdown so you can easily analyse the problem and see that the issue is nowhere else, but the JavaScript. If you are not new to this please just scroll to the javaScript CUSTOM-SEARCH.JS section.
I found an article on how to add ajax search filters.
searchform.php
(Standard Search Form)
THE STANDARD SEARCH FORM
Before the filters, I edited the search form so a user can search based on post type (opone,optwo,opthree).
<form role="search" method="get" class="search-form" action="<?php echo esc_url( home_url( '/' ) ); ?>">
<select id="drpdwn_search">
<option value="any" selected>Choose Type</option>
<option value="opone">Option 1</option>
<option value="optwo">Option 2</option>
<option value="opthree">Option 3</option>
</select>
<input type="search" class="search-field form-control" name="s" placeholder="Search" value="<?php echo esc_attr( get_search_query() ); ?>" title="<?php _ex( 'Search for:', 'label', 'wp-bootstrap-starter' ); ?>">
<input type="hidden" name="post_type" value="any" />
<input type="submit" class="search-submit btn btn-default" value="<?php echo esc_attr_x( 'Search', 'submit button', 'wp-bootstrap-starter' ); ?>">
</form>
The site was working normally where the search results were shown and InfiniteScroll was used for pagination, but I wanted to be able to filter the search results so I tried creating an ajax search filter form.
search.php
(AJAX Filter Form/Search Results template)
Ignore $actual_link
- Since I am using a staging server without SSL $actual_link
variable supports HTTP and HTTPS, but will change in production if the variable gets used.
Query Moved
I moved the query into a function according to the instructions to implement the ajax filter so it is no longer in search.php.
Ajax Filter Form
For the AJAX filter form, I got the search term using the get_search_query() function in the search field input and post type (opone,optwo,opthree) from the query string in the URL (/?s={SEARCH TERM}&post_type={POST TYPE})and stored it as a variable ($param). Based on the post type selected, only its custom taxonomy "categories" ('opone_cat','optwo_cat','opthree_cat') will be an option in the AJAX filter form. Then created radio buttons in the AJAX filter form so users can display results ascending or descending.
Ajax search filter form and response div (.scroll-content
) in search results template (search.php):
<?php
$actual_link = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
$param = filter_input(INPUT_GET, 'post_type', FILTER_SANITIZE_URL);
?>
<form action="<?php echo site_url() ?>/wp-admin/admin-ajax.php" method="POST" id="filter">
<input type="search" class="search-field form-control" name="s" placeholder="Search" value="<?php echo esc_attr( get_search_query() ); ?>" title="<?php _ex( 'Search for:', 'label', 'wp-bootstrap-starter' ); ?>">
<input type="hidden" class="form-control" name="post_type" value="<?php echo $param; ?>" />
<?php
if('opone' == $param) {
if( $terms = get_terms( array(
'taxonomy' => 'opone_cat',
'orderby' => 'name'
) ) ) :
// if categories exist, display the dropdown
echo '<select name="categoryfilter" class="form-select" aria-label="Default select example"><option value="">Select category...</option>';
foreach ( $terms as $term ) :
echo '<option value="' . $term->term_id . '">' . $term->name . '</option>'; // ID of the category as an option value
endforeach;
echo '</select>';
endif;
} else if('optwo' == $param) {
if( $terms = get_terms( array(
'taxonomy' => 'optwo_cat',
'orderby' => 'name'
) ) ) :
// if categories exist, display the dropdown
echo '<select name="categoryfilter" class="form-select" aria-label="Default select example"><option value="">Select category...</option>';
foreach ( $terms as $term ) :
echo '<option value="' . $term->term_id . '">' . $term->name . '</option>'; // ID of the category as an option value
endforeach;
echo '</select>';
endif;
} else if('opthree' == $param) {
if( $terms = get_terms( array(
'taxonomy' => 'opthree_cat',
'orderby' => 'name'
) ) ) :
// if categories exist, display the dropdown
echo '<select name="categoryfilter" class="form-select" aria-label="Default select example"><option value="">Select category...</option>';
foreach ( $terms as $term ) :
echo '<option value="' . $term->term_id . '">' . $term->name . '</option>'; // ID of the category as an option value
endforeach;
echo '</select>';
endif;
} else {
if( $terms = get_terms( array(
'taxonomy' => array('opone_cat','optwo_cat','opthree_cat'),
'orderby' => 'date'
) ) ) :
// if categories exist, display the dropdown
echo '<select name="categoryfilter" class="form-select" aria-label="Default select example"><option value="">Select category...</option>';
foreach ( $terms as $term ) :
echo '<option value="' . $term->term_id . '">' . $term->name . '</option>'; // ID of the category as an option value
endforeach;
echo '</select>';
endif;
}
?>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" id="asc" name="date" value="ASC" />
<label class="form-check-label" for="asc">Date: Ascending</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" id="dsc" name="date" value="DESC" selected="selected" />
<label class="form-check-label" for="dsc">Date: Descending</label>
</div>
<button class="btn btn-primary btn-filter btn-lg">Apply filter</button>
<input type="hidden" name="action" value="myfilter">
</form>
</div>
<div class="scroll-content col-sm-12 col-md-9"></div>
If you look at the ajax search filter form above in search.php (search results template), we have a hidden action input with the value myfilter.
<input type="hidden" name="action" value="myfilter">
functions.php
myfilter
- the name of the AJAX action callback being fired.
In the instructions to implement the Ajax search filter, we are to put the query in a function and add these ajax action hooks to handle the request. One hook is for logged-in (wp_ajax_myfilter
) users and the other for non-logged-in (wp_ajax_nopriv_myfilter
) users.
add_action('wp_ajax_myfilter', 'search_filter_function');
add_action('wp_ajax_nopriv_myfilter', 'search_filter_function');
The function below has the search query loop. From the ajax filter form, we were able to get the search term $_POST['s']
, post type $_POST['post_type']
, date $_POST['date']
, and post type taxonomies $_POST['categoryfilter']
as arguments for the search query loop.
The other arguments ($args) include the standard paging variable to use for the paged argument (pagination), set post_status
to make sure only published posts are in the results, and posts_per_page to show only six posts per page.
As you can see I am using get_template_part()
to add the content (posts) and pagination.
add_action('wp_ajax_myfilter', 'search_filter_function');
add_action('wp_ajax_nopriv_myfilter', 'search_filter_function');
function search_filter_function(){
global $wp_post_types, $wp_query;
$wp_post_types['page']->exclude_from_search = true;
$paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
$args = array(
's' => $_POST['s'],
'post_type' => $_POST['post_type'],
'post_status' => 'publish',
'posts_per_page' => 6,
'orderby' => 'date', // we will sort posts by date
'order' => $_POST['date'], // ASC or DESC
'paged' => $paged
);
// for taxonomies / categories
if( isset( $_POST['categoryfilter'] ) )
$args['tax_query'] = array(
'relation' => 'OR',
array(
'taxonomy' => 'opone',
'field' => 'id',
'terms' => $_POST['categoryfilter'],
),
array(
'taxonomy' => 'optwo',
'field' => 'id',
'terms' => $_POST['categoryfilter'],
),
array(
'taxonomy' => 'opthree',
'field' => 'id',
'terms' => $_POST['categoryfilter'],
),
);
$search = new WP_Query( $args );
if ( $search->have_posts() ) : while ( $search->have_posts() ) : $search->the_post();
get_template_part( 'template-parts/content', 'search' );
endwhile;
get_template_part( 'template-parts/pagination', 'notabs' );
else :
get_template_part( 'template-parts/content', 'none' );
endif;
die();
}
/TEMPLATE-PARTS/CONTENT-SEARCH.PHP
The template parts were working before adding the ajax filter and as you can see I know better not to add any custom scripts to these particular files.
I also excluded the page
post type as well.
Here is the content template:
<article id="post-<?php the_ID(); ?>" <?php post_class('scroll-post'); ?> data-category="<?php echo get_post_type(); ?>">
<div class="post-thumbnail">
<?php the_post_thumbnail(); ?>
</div>
<header class="entry-header">
<?php
the_title( '<h2 class="entry-title"><a href="' . esc_url( get_permalink() ) . '" rel="bookmark">', '</a></h2>' );
?>
<div class="entry-meta">
<?php wp_bootstrap_starter_posted_on(); ?>
</div><!-- .entry-meta -->
</header><!-- .entry-header -->
<!-- <div class="entry-content">excerpt</div> -->
<footer class="entry-footer">
<?php wp_bootstrap_starter_entry_footer(); ?>
</footer><!-- .entry-footer -->
</article><!-- #post-## -->
/TEMPLATE-PARTS/PAGINATION-NOTABS.PHP
Here is the pagination template:
<?php if(wp_script_is( 'infinite', 'enqueued' )) : ?>
<div class="page-load-status">
<div class="loader-ellips infinite-scroll-request">
<span class="loader-ellips__dot"></span>
<span class="loader-ellips__dot"></span>
<span class="loader-ellips__dot"></span>
<span class="loader-ellips__dot"></span>
</div>
<p class="infinite-scroll-last">End of content</p>
<p class="infinite-scroll-error">No more pages to load</p>
</div>
<p>
<button class="btn btn-primary btn-scroll btn-lg">View more</button>
</p>
<div id="nav-below infinite" class="pagination">
<div class="next-post"><?php next_posts_link() ?></div>
</div>
<?php endif; ?>
FUNCTIONS.PHP
(JavaScript)
I had already enqueued the Isotope script and its layout fitRows. Also added the InfiniteScroll script and registered/localized a custom javaScript file (custom-search.js
) so I can pass a PHP variable ($search_param
) with the search term since it will be needed in the custom-search.js
file.
Versions:
wp_enqueue_script('isotope', get_template_directory_uri() . '/inc/assets/js/isotope/isotope.pkgd.min.js', array('jquery'), '', false);
wp_enqueue_script('fitrows', get_template_directory_uri() . '/inc/assets/js/isotope/layout-modes/fit-rows.js', array('isotope'), '', false);
if( is_search() ) {
wp_register_script( 'custom-search', get_template_directory_uri() . '/inc/assets/js/isotope/archive-search.js', array('infinite'), '', true );
$search_query = get_search_query();
$search_param = array('search_term' => $search_query);
wp_enqueue_script('infinite', get_template_directory_uri() . '/inc/assets/js/isotope/infinitescroll.pkgd.min.js', array('jquery'), '', false);
wp_enqueue_script('custom-search', get_template_directory_uri() . '/inc/assets/js/isotope/custom-search.js', array('infinite'), '', true);
wp_localize_script( 'custom-search', 'searchParam', $search_param );
}
CUSTOM-SEARCH.JS
(JavaScript)
Great news! The filter works but...
Key problem - Isotope and InfiniteScroll does not work on the filtered posts and posts do not show without the filter being applied.
Isotope and InfiniteScroll stopped working after adding the AJAX filter form in the search results template (search.php
) and after moving the query to a function (functions.php
). I copied the Ajax call from the instructions and added above the previously working InfiniteScroll and Isotope below in custom-search.js
.
**From what I know, I need to apply the Isotope fitRows layout when appending posts. I think InfiniteScroll for pagination is a problem due to it only being initialised here based on the post type in the URL query string.
I also need to show the initial search results by default (before filters are applied) and hide (fade out) the initial search results before the filtered posts are displayed.**
This is where I need help.
jQuery(window).on('load', function () {
jQuery('#filter').submit(function () {
var filter = jQuery('#filter');
jQuery.ajax({
url: filter.attr('action'),
data: filter.serialize(),
type: filter.attr('method'), // POST
beforeSend: function (xhr) {
filter.find('.btn-filter').text('Processing...');
},
success: function (data) {
filter.find('.btn-filter').text('Apply filter');
jQuery('.scroll-content').html(data); // insert data
} //success
}); // jQuery ajax
return false;
}); //submit function
let currentLocation = window.location.href;
const ptParams = new Proxy(new URLSearchParams(window.location.search), {get: (searchParams, prop) => searchParams.get(prop),});
let post_type_value = ptParams.post_type;
let $scroll_container = jQuery('.scroll-content');
let fhsFit = $scroll_container.data('isotope');
$scroll_container.isotope({
layoutMode: 'fitRows',
itemSelector: '.scroll-post'
}); //isotope
if ( post_type_value === 'opone' || post_type_value === 'optwo' || post_type_value === 'opthree' ) {
$scroll_container.infiniteScroll({
path: 'page/{{#}}/?s=' + searchParam.search_term + '&post_type=' + post_type_value,
append: '.scroll-post',
button: '.btn-scroll',
outlayer: fhsFit,
loadOnScroll: false,
scrollThreshold: 300,
status: '.page-load-status',
hideNav: '.pagination'
}); //infinite scroll
} else {
$scroll_container.infiniteScroll({
path: 'page/{{#}}/?s=' + searchParam.search_term + '&post_type=any',
append: '.scroll-post',
button: '.btn-scroll',
outlayer: fhsFit,
loadOnScroll: false,
scrollThreshold: 300,
status: '.page-load-status',
hideNav: '.pagination'
}); //infinite scroll
} //if statement
jQuery('.btn-scroll').on('click', function () {
$scroll_container.on('load.infiniteScroll', function (event) {
$scroll_container.isotope('layout');
jQuery('.page-load-status').detach().appendTo(jQuery('.scroll-content'));
}); //on load function
}); // on click function
}); // window on load
Custom Ajax Search filter for WordPress Search
I want to give a complete breakdown so you can easily analyse the problem and see that the issue is nowhere else, but the JavaScript. If you are not new to this please just scroll to the javaScript CUSTOM-SEARCH.JS section.
I found an article on how to add ajax search filters.
searchform.php
(Standard Search Form)
THE STANDARD SEARCH FORM
Before the filters, I edited the search form so a user can search based on post type (opone,optwo,opthree).
<form role="search" method="get" class="search-form" action="<?php echo esc_url( home_url( '/' ) ); ?>">
<select id="drpdwn_search">
<option value="any" selected>Choose Type</option>
<option value="opone">Option 1</option>
<option value="optwo">Option 2</option>
<option value="opthree">Option 3</option>
</select>
<input type="search" class="search-field form-control" name="s" placeholder="Search" value="<?php echo esc_attr( get_search_query() ); ?>" title="<?php _ex( 'Search for:', 'label', 'wp-bootstrap-starter' ); ?>">
<input type="hidden" name="post_type" value="any" />
<input type="submit" class="search-submit btn btn-default" value="<?php echo esc_attr_x( 'Search', 'submit button', 'wp-bootstrap-starter' ); ?>">
</form>
The site was working normally where the search results were shown and InfiniteScroll was used for pagination, but I wanted to be able to filter the search results so I tried creating an ajax search filter form.
search.php
(AJAX Filter Form/Search Results template)
Ignore $actual_link
- Since I am using a staging server without SSL $actual_link
variable supports HTTP and HTTPS, but will change in production if the variable gets used.
Query Moved
I moved the query into a function according to the instructions to implement the ajax filter so it is no longer in search.php.
Ajax Filter Form
For the AJAX filter form, I got the search term using the get_search_query() function in the search field input and post type (opone,optwo,opthree) from the query string in the URL (http://somedomain.com/?s={SEARCH TERM}&post_type={POST TYPE})and stored it as a variable ($param). Based on the post type selected, only its custom taxonomy "categories" ('opone_cat','optwo_cat','opthree_cat') will be an option in the AJAX filter form. Then created radio buttons in the AJAX filter form so users can display results ascending or descending.
Ajax search filter form and response div (.scroll-content
) in search results template (search.php):
<?php
$actual_link = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
$param = filter_input(INPUT_GET, 'post_type', FILTER_SANITIZE_URL);
?>
<form action="<?php echo site_url() ?>/wp-admin/admin-ajax.php" method="POST" id="filter">
<input type="search" class="search-field form-control" name="s" placeholder="Search" value="<?php echo esc_attr( get_search_query() ); ?>" title="<?php _ex( 'Search for:', 'label', 'wp-bootstrap-starter' ); ?>">
<input type="hidden" class="form-control" name="post_type" value="<?php echo $param; ?>" />
<?php
if('opone' == $param) {
if( $terms = get_terms( array(
'taxonomy' => 'opone_cat',
'orderby' => 'name'
) ) ) :
// if categories exist, display the dropdown
echo '<select name="categoryfilter" class="form-select" aria-label="Default select example"><option value="">Select category...</option>';
foreach ( $terms as $term ) :
echo '<option value="' . $term->term_id . '">' . $term->name . '</option>'; // ID of the category as an option value
endforeach;
echo '</select>';
endif;
} else if('optwo' == $param) {
if( $terms = get_terms( array(
'taxonomy' => 'optwo_cat',
'orderby' => 'name'
) ) ) :
// if categories exist, display the dropdown
echo '<select name="categoryfilter" class="form-select" aria-label="Default select example"><option value="">Select category...</option>';
foreach ( $terms as $term ) :
echo '<option value="' . $term->term_id . '">' . $term->name . '</option>'; // ID of the category as an option value
endforeach;
echo '</select>';
endif;
} else if('opthree' == $param) {
if( $terms = get_terms( array(
'taxonomy' => 'opthree_cat',
'orderby' => 'name'
) ) ) :
// if categories exist, display the dropdown
echo '<select name="categoryfilter" class="form-select" aria-label="Default select example"><option value="">Select category...</option>';
foreach ( $terms as $term ) :
echo '<option value="' . $term->term_id . '">' . $term->name . '</option>'; // ID of the category as an option value
endforeach;
echo '</select>';
endif;
} else {
if( $terms = get_terms( array(
'taxonomy' => array('opone_cat','optwo_cat','opthree_cat'),
'orderby' => 'date'
) ) ) :
// if categories exist, display the dropdown
echo '<select name="categoryfilter" class="form-select" aria-label="Default select example"><option value="">Select category...</option>';
foreach ( $terms as $term ) :
echo '<option value="' . $term->term_id . '">' . $term->name . '</option>'; // ID of the category as an option value
endforeach;
echo '</select>';
endif;
}
?>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" id="asc" name="date" value="ASC" />
<label class="form-check-label" for="asc">Date: Ascending</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" id="dsc" name="date" value="DESC" selected="selected" />
<label class="form-check-label" for="dsc">Date: Descending</label>
</div>
<button class="btn btn-primary btn-filter btn-lg">Apply filter</button>
<input type="hidden" name="action" value="myfilter">
</form>
</div>
<div class="scroll-content col-sm-12 col-md-9"></div>
If you look at the ajax search filter form above in search.php (search results template), we have a hidden action input with the value myfilter.
<input type="hidden" name="action" value="myfilter">
functions.php
myfilter
- the name of the AJAX action callback being fired.
In the instructions to implement the Ajax search filter, we are to put the query in a function and add these ajax action hooks to handle the request. One hook is for logged-in (wp_ajax_myfilter
) users and the other for non-logged-in (wp_ajax_nopriv_myfilter
) users.
add_action('wp_ajax_myfilter', 'search_filter_function');
add_action('wp_ajax_nopriv_myfilter', 'search_filter_function');
The function below has the search query loop. From the ajax filter form, we were able to get the search term $_POST['s']
, post type $_POST['post_type']
, date $_POST['date']
, and post type taxonomies $_POST['categoryfilter']
as arguments for the search query loop.
The other arguments ($args) include the standard paging variable to use for the paged argument (pagination), set post_status
to make sure only published posts are in the results, and posts_per_page to show only six posts per page.
As you can see I am using get_template_part()
to add the content (posts) and pagination.
add_action('wp_ajax_myfilter', 'search_filter_function');
add_action('wp_ajax_nopriv_myfilter', 'search_filter_function');
function search_filter_function(){
global $wp_post_types, $wp_query;
$wp_post_types['page']->exclude_from_search = true;
$paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
$args = array(
's' => $_POST['s'],
'post_type' => $_POST['post_type'],
'post_status' => 'publish',
'posts_per_page' => 6,
'orderby' => 'date', // we will sort posts by date
'order' => $_POST['date'], // ASC or DESC
'paged' => $paged
);
// for taxonomies / categories
if( isset( $_POST['categoryfilter'] ) )
$args['tax_query'] = array(
'relation' => 'OR',
array(
'taxonomy' => 'opone',
'field' => 'id',
'terms' => $_POST['categoryfilter'],
),
array(
'taxonomy' => 'optwo',
'field' => 'id',
'terms' => $_POST['categoryfilter'],
),
array(
'taxonomy' => 'opthree',
'field' => 'id',
'terms' => $_POST['categoryfilter'],
),
);
$search = new WP_Query( $args );
if ( $search->have_posts() ) : while ( $search->have_posts() ) : $search->the_post();
get_template_part( 'template-parts/content', 'search' );
endwhile;
get_template_part( 'template-parts/pagination', 'notabs' );
else :
get_template_part( 'template-parts/content', 'none' );
endif;
die();
}
/TEMPLATE-PARTS/CONTENT-SEARCH.PHP
The template parts were working before adding the ajax filter and as you can see I know better not to add any custom scripts to these particular files.
I also excluded the page
post type as well.
Here is the content template:
<article id="post-<?php the_ID(); ?>" <?php post_class('scroll-post'); ?> data-category="<?php echo get_post_type(); ?>">
<div class="post-thumbnail">
<?php the_post_thumbnail(); ?>
</div>
<header class="entry-header">
<?php
the_title( '<h2 class="entry-title"><a href="' . esc_url( get_permalink() ) . '" rel="bookmark">', '</a></h2>' );
?>
<div class="entry-meta">
<?php wp_bootstrap_starter_posted_on(); ?>
</div><!-- .entry-meta -->
</header><!-- .entry-header -->
<!-- <div class="entry-content">excerpt</div> -->
<footer class="entry-footer">
<?php wp_bootstrap_starter_entry_footer(); ?>
</footer><!-- .entry-footer -->
</article><!-- #post-## -->
/TEMPLATE-PARTS/PAGINATION-NOTABS.PHP
Here is the pagination template:
<?php if(wp_script_is( 'infinite', 'enqueued' )) : ?>
<div class="page-load-status">
<div class="loader-ellips infinite-scroll-request">
<span class="loader-ellips__dot"></span>
<span class="loader-ellips__dot"></span>
<span class="loader-ellips__dot"></span>
<span class="loader-ellips__dot"></span>
</div>
<p class="infinite-scroll-last">End of content</p>
<p class="infinite-scroll-error">No more pages to load</p>
</div>
<p>
<button class="btn btn-primary btn-scroll btn-lg">View more</button>
</p>
<div id="nav-below infinite" class="pagination">
<div class="next-post"><?php next_posts_link() ?></div>
</div>
<?php endif; ?>
FUNCTIONS.PHP
(JavaScript)
I had already enqueued the Isotope script and its layout fitRows. Also added the InfiniteScroll script and registered/localized a custom javaScript file (custom-search.js
) so I can pass a PHP variable ($search_param
) with the search term since it will be needed in the custom-search.js
file.
Versions:
wp_enqueue_script('isotope', get_template_directory_uri() . '/inc/assets/js/isotope/isotope.pkgd.min.js', array('jquery'), '', false);
wp_enqueue_script('fitrows', get_template_directory_uri() . '/inc/assets/js/isotope/layout-modes/fit-rows.js', array('isotope'), '', false);
if( is_search() ) {
wp_register_script( 'custom-search', get_template_directory_uri() . '/inc/assets/js/isotope/archive-search.js', array('infinite'), '', true );
$search_query = get_search_query();
$search_param = array('search_term' => $search_query);
wp_enqueue_script('infinite', get_template_directory_uri() . '/inc/assets/js/isotope/infinitescroll.pkgd.min.js', array('jquery'), '', false);
wp_enqueue_script('custom-search', get_template_directory_uri() . '/inc/assets/js/isotope/custom-search.js', array('infinite'), '', true);
wp_localize_script( 'custom-search', 'searchParam', $search_param );
}
CUSTOM-SEARCH.JS
(JavaScript)
Great news! The filter works but...
Key problem - Isotope and InfiniteScroll does not work on the filtered posts and posts do not show without the filter being applied.
Isotope and InfiniteScroll stopped working after adding the AJAX filter form in the search results template (search.php
) and after moving the query to a function (functions.php
). I copied the Ajax call from the instructions and added above the previously working InfiniteScroll and Isotope below in custom-search.js
.
**From what I know, I need to apply the Isotope fitRows layout when appending posts. I think InfiniteScroll for pagination is a problem due to it only being initialised here based on the post type in the URL query string.
I also need to show the initial search results by default (before filters are applied) and hide (fade out) the initial search results before the filtered posts are displayed.**
This is where I need help.
jQuery(window).on('load', function () {
jQuery('#filter').submit(function () {
var filter = jQuery('#filter');
jQuery.ajax({
url: filter.attr('action'),
data: filter.serialize(),
type: filter.attr('method'), // POST
beforeSend: function (xhr) {
filter.find('.btn-filter').text('Processing...');
},
success: function (data) {
filter.find('.btn-filter').text('Apply filter');
jQuery('.scroll-content').html(data); // insert data
} //success
}); // jQuery ajax
return false;
}); //submit function
let currentLocation = window.location.href;
const ptParams = new Proxy(new URLSearchParams(window.location.search), {get: (searchParams, prop) => searchParams.get(prop),});
let post_type_value = ptParams.post_type;
let $scroll_container = jQuery('.scroll-content');
let fhsFit = $scroll_container.data('isotope');
$scroll_container.isotope({
layoutMode: 'fitRows',
itemSelector: '.scroll-post'
}); //isotope
if ( post_type_value === 'opone' || post_type_value === 'optwo' || post_type_value === 'opthree' ) {
$scroll_container.infiniteScroll({
path: 'page/{{#}}/?s=' + searchParam.search_term + '&post_type=' + post_type_value,
append: '.scroll-post',
button: '.btn-scroll',
outlayer: fhsFit,
loadOnScroll: false,
scrollThreshold: 300,
status: '.page-load-status',
hideNav: '.pagination'
}); //infinite scroll
} else {
$scroll_container.infiniteScroll({
path: 'page/{{#}}/?s=' + searchParam.search_term + '&post_type=any',
append: '.scroll-post',
button: '.btn-scroll',
outlayer: fhsFit,
loadOnScroll: false,
scrollThreshold: 300,
status: '.page-load-status',
hideNav: '.pagination'
}); //infinite scroll
} //if statement
jQuery('.btn-scroll').on('click', function () {
$scroll_container.on('load.infiniteScroll', function (event) {
$scroll_container.isotope('layout');
jQuery('.page-load-status').detach().appendTo(jQuery('.scroll-content'));
}); //on load function
}); // on click function
}); // window on load
I was able to figure it out...well as long as jQuery and AJAX are alive and utilized by WordPress (I know it may be best to use WordPress REST API, but ...this works)!
GET STARTED
I did have to edit searchform.php, functions.php, search.php, and custom-search.js. Please make sure taxonomies and post types are queryable (public) and query vars are set to true. If you used CPT UI you can edit them here:
http://domain.com/wp-admin/admin.php?page=cptui_manage_taxonomies&action=edit
Set the 'Public Queryable' option to 'true' and 'Query Var' to 'true'.
SEARCHFORM.PHP
I had to turn off auto-complete on the search and ajax filter form in case of users using the back button since the InfiniteScroll option for history by default is set to true.
<form role="search" method="get" class="search-form" action="<?php echo esc_url( home_url( '/' ) ); ?>" autocomplete="off">
<select id="drpdwn_search">
<option value="any" selected>All</option>
<option value="opone">One</option>
<option value="optwo">Two</option>
<option value="opthree">Three</option>
</select>
<input type="search" class="search-field form-control" name="s" placeholder="Search" value="<?php echo esc_attr( get_search_query() ); ?>" title="<?php _ex( 'Search for:', 'label', 'wp-bootstrap-starter' ); ?>">
<input type="hidden" name="post_type" value="any" />
<input type="submit" class="search-submit btn btn-default" value="<?php echo esc_attr_x( 'Search', 'submit button', 'wp-bootstrap-starter' ); ?>">
</form>
As you can see if a user does not select a post type it defaults to any' post type for the value.
<input type="hidden" name="post_type" value="any" />
SEARCH.PHP
The ajax filter form fields should all be required through HTML.
Ajax Filter - Form Tag
I added the autocomplete attribute on the form tag and set it to off.
Ajax Filter Form - Taxonomies
FYI: My taxonomies are custom post type categories created with CPT UI - as you can see throughout the whole thing I did not use WP Core Categories.
In the ajax filter form, I edited the taxonomies filter to get all terms when the post type is set to any in the else statement.
Ajax Filter Form - Search Field
I then set the search-field input type to hidden so they can only filter the search term from the form in searchform.php. I plan on displaying the search form in the header (header.php) so it can be everywhere so... the search field in the ajax filter form can remain hidden unless you want to filter with it on the search results page).
Default Search Results Query (Not in AJAX Callback Duh..)
I use the default search query loop ($wp_query) for non-ajax-filtered results. We do not need to use a custom search query. This link explains the mistake that I made. The loop in the AJAX callback function uses query_posts() although many articles state not to use it. One reason is that it breaks paging, but it is okay since I will use a hard-coded URL path for the AJAX InfiniteScroll instance.
Ajax Filter Search Results Query - Container DIV
I wanted the ajax call to have its own container, so I added a div with only the same bootstrap classes as the default search query container, but with the id (#ajax_container).
<section class="row">
<div id="secondary" class="content-area col-sm-12 col-md-2">
<?php
$actual_link = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
$param = filter_input(INPUT_GET, 'post_type', FILTER_SANITIZE_URL);
?>
<form action="<?php echo site_url() ?>/wp-admin/admin-ajax.php" method="POST" id="filter" autocomplete="off">
<input type="hidden" class="search-field form-control" name="s" placeholder="Search" value="<?php echo esc_attr(get_search_query()); ?>" title="<?php _ex('Search for:', 'label', 'wp-bootstrap-starter'); ?>">
<input type="hidden" class="form-control" name="post_type" value="<?php echo $param; ?>" />
<?php
if ('font' == $param) {
if ($terms = get_terms(array('taxonomy' => 'font_cat', 'hide_empty' => false, 'orderby' => 'name'))):
// if categories exist, display the dropdown
echo '<select name="categoryfilter" class="form-select" aria-label="One categories"><option value="">Select category...</option>';
foreach ($terms as $term):
echo '<option value="' . $term->term_id . '">' . $term->name . '</option>'; // ID of the category as an option value
endforeach;
echo '</select>';
endif;
} else if ('snipp' == $param) {
if ($terms = get_terms(array('taxonomy' => 'snipp_cat', 'hide_empty' => false, 'orderby' => 'name'))):
// if categories exist, display the dropdown
echo '<select name="categoryfilter" class="form-select" aria-label="Two categories"><option value="">Select category...</option>';
foreach ($terms as $term):
echo '<option value="' . $term->term_id . '">' . $term->name . '</option>'; // ID of the category as an option value
endforeach;
echo '</select>';
endif;
} else if ('post' == $param) {
if ($terms = get_terms(array('taxonomy' => 'blog_cat', 'hide_empty' => false, 'orderby' => 'name'))):
// if categories exist, display the dropdown
echo '<select name="categoryfilter" class="form-select" aria-label="Three categories"><option value="">Select category...</option>';
foreach ($terms as $term):
echo '<option value="' . $term->term_id . '">' . $term->name . '</option>'; // ID of the category as an option value
endforeach;
echo '</select>';
endif;
} else {
$term_args = array('taxonomy' => array('font_cat', 'snipp_cat', 'blog_cat'), 'hide_empty' => false, 'fields' => 'all', 'count' => true,);
$term_query = new WP_Term_Query($term_args);
$term_taxs = $term_args["taxonomy"];
echo '<select name="categoryfilter" class="form-select" aria-label="All categories" required><option value="">Select category...</option>';
foreach ($term_taxs as $term_tax):
if ($term_tax === 'font_cat'):
echo '<option value="" disabled>Font Categories</option>';
foreach ($term_query->terms as $term):
if ($term->taxonomy == 'font_cat'):
echo '<option value="' . $term->term_id . '">' . $term->name . '</option>';
endif;
endforeach;
endif;
if ($term_tax === 'snipp_cat'):
echo '<option value="" disabled>Snippet Categories</option>';
foreach ($term_query->terms as $term):
if ($term->taxonomy == 'snipp_cat'):
echo '<option value="' . $term->term_id . '">' . $term->name . '</option>';
endif;
endforeach;
endif;
if ($term_tax === 'blog_cat'):
echo '<option value="" disabled>Blog Categories</option>';
foreach ($term_query->terms as $term):
if ($term->taxonomy == 'blog_cat'):
echo '<option value="' . $term->term_id . '">' . $term->name . '</option>';
endif;
endforeach;
endif;
endforeach;
echo '</select>';
}
?>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" id="asc" name="date" value="asc" />
<label class="form-check-label" for="asc">Date: Ascending</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" id="desc" name="date" value="desc" selected="selected" required />
<label class="form-check-label" for="dsc">Date: Descending</label>
</div>
<button class="btn btn-primary btn-filter btn-lg">Apply filter</button>
<input type="hidden" name="action" value="myfilter">
</form>
</div>
<div class="scroll-content col-sm-12 col-md-10">
<?php
global $wp_post_types, $wp_query;
$wp_post_types['page']->exclude_from_search = true;
if (have_posts()):
while (have_posts()):
the_post();
get_template_part('template-parts/content', 'archive');
endwhile;
get_template_part('template-parts/pagination', 'notabs');
else:
get_template_part('template-parts/content', 'none');
endif;
?>
</div><!-- .scroll-content -->
<div id="ajax_container" class="col-sm-12 col-md-10"></div>
</section> <!-- row -->
FUNCTIONS.PHP
AJAX Callback Function - Protocol
Since HTTP and HTTPS are flexible for my domain, I had to specify the protocol for the ajax URL. Of course, HTTPS will only be used in production so this can be removed if HTTPS is forced.
AJAX Callback Function - Query Loop
As stated, since I decided to modify the main query for the search - I used query_posts() and wp_reset_query() after pagination.
add_action('wp_ajax_myfilter', 'search_filter_function');
add_action('wp_ajax_nopriv_myfilter', 'search_filter_function');
function search_filter_function(){
global $wp_post_types, $wp_query;
$wp_post_types['page']->exclude_from_search = true;
$paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
$protocol = isset( $_SERVER['HTTPS'] ) ? 'https://' : 'http://';
$args = array(
'ajaxurl' => admin_url( 'admin-ajax.php', $protocol ),
'query' => $wp_query->query,
's' => $_POST['s'],
'post_type' => $_POST['post_type'],
'post_status' => 'publish',
'posts_per_page' => 6,
'orderby' => 'date',
'order' => $_POST['date'], // ASC or DESC
'paged' => $paged
);
// for taxonomies
if( isset( $_POST['categoryfilter'] ) ) {
$args['tax_query'] = array(
'relation' => 'OR',
array(
'taxonomy' => 'font_cat',
'field' => 'id',
'terms' => $_POST['categoryfilter'],
),
array(
'taxonomy' => 'snipp_cat',
'field' => 'id',
'terms' => $_POST['categoryfilter'],
),
array(
'taxonomy' => 'blog_cat',
'field' => 'id',
'terms' => $_POST['categoryfilter'],
),
);
}
query_posts( $args );
if ( have_posts() ) : while ( have_posts() ) : the_post();
get_template_part( 'template-parts/content', 'search' );
endwhile;
get_template_part( 'template-parts/pagination', 'notabs' );
wp_reset_query();
else :
get_template_part( 'template-parts/content', 'none' );
endif;
die();
}
FUNCTIONS.PHP (Continued)
Next Link for Pagination
I needed to add a class to the next paging link so I added a filter to the next_post_link()
function for the non-ajax InfiniteScroll instance.
FYI: This is important for the non-ajax InfiniteScroll instance path option since I do not want a hard-coded URL.
add_filter('next_posts_link_attributes', 'next_link_class_attribute');
function next_link_class_attribute() {
return 'class="next-post-link"';
}
In order to avoid an empty search field, I added this function to send the user to the 404 page if no search term is entered.
//if search is empty to display 404
add_action( 'pre_get_posts', function ( $q )
{
if($q->is_search() // Only target the search page
) {
// Get the search terms
$search_terms = $q->get( 's' );
// Set a 404 if s is empty
if ( !$search_terms ) {
add_action( 'wp', function () use ( $q )
{
$q->set_404();
status_header(404);
nocache_headers();
});
}
}
});
I also set 404 if there are only one or more invalid query vars. I do not recommend this function if you have not registered custom query vars. https://gist.github.com/carlodaniele/1ca4110fa06902123349a0651d454057
//set 404 only if one or more invalid query vars (invalid taxonomy names but not only) are in the query
add_action( 'template_redirect', 'my_page_template_redirect' );
function my_page_template_redirect() {
global $wp_query, $wp;
// this get an array of the query vars in the url
parse_str( parse_url ( add_query_arg( array() ), PHP_URL_QUERY ), $qv);
if ( ! empty( $qv ) ) { // if there are some query vars in the url
$queried = array_keys( $qv ); // get the query vars name
$valid = $wp->public_query_vars; // this are the valid query vars accepted by WP
// intersect array to retrieve the queried vars tha are included in the valid vars
$good = array_intersect($valid, $queried);
// if there are no good query vars or if there are at least one not valid var
if ( empty($good) || count($good) !== count($queried) ) {
$wp_query->set_404();
status_header(404);
nocache_headers();
}
}
}
UPGRADE InfiniteScroll to v4 I did upgrade to InfiniteScroll v4 because loadNextPage returns a Promise, so basically I can use then() in case I want to do something after each load of posts. Also, because of its backward compatibility with v3.
CUSTOM-SEARCH.JS
AJAX Filter - Submit without Reload
I had to bind the filter button to a click event so we can apply filters without having to reload the entire page.
AJAX Filter - InfiniteScroll and Isotope Sessions
Once a user clicks submit on the filter, I use the InfiniteScroll and Isotope destroy method on both containers ($scroll_container and $ajax_container) in case isotope or infinite scroll already exists (destroy session on success to first create new).
FYI: .data('inifiniteScroll')
and InfiniteScroll.data( element )
does not work in v3 and v4 so I cannot check if InifinteScroll is initialized before using destroy method so error will show in console.
I then empty the AJAX container (#ajax_container.empty()) as well to make sure new results are displayed every time the user filters with the ajax filter form.
AJAX Filter - Isotope I was able to get Isotope to work on the results by turning the response (data) to a jQuery object before inserting the data with Isotope.
let $data = $(data); // store data in jQuery object
$ajax_container.append($data);
$ajax_container.isotope('insert', $data );
AJAX Filter - InfiniteScroll
I did find a solution to fix InfiniteScroll by setting the path correctly - found through console logging the data to get the query strings for pagination (console.log(filter.serialize())).
As stated prior, now only the AJAX InfiniteScroll instance uses a hard-coded path option. I tried creating the path with just public query vars but had issues with paged results after setting pretty links (customizing the slug - rewrites) for post types and taxonomies.
This was done with the CPT UI plugin: http://domain.com/wp-admin/admin.php?page=cptui_manage_taxonomies&action=edit
I set 'Rewrite' to 'true', added 'Custom Rewrite Slug' text, and set 'Rewrite With Front' to 'true'.
As stated prior, I still would have to manually build the path URL for the AJAX InfiniteScroll instance.
The good news is paging is possible with rewrites based on my setup.
http://domain.com/[taxonomy rewrite slug]/[taxonomy term]/page/{{#}}/?s=[search term]&orderby=date&order=[asc or desc]
As you can see the path option for the AJAX InfiniteScroll Instance is now set properly.
jQuery(window).on('load', function () {
let $scroll_container = jQuery('.scroll-content');
$scroll_container.isotope({
layoutMode: 'fitRows',
itemSelector: '.scroll-post'
}); //isotope
let currentLocation = window.location.href;
const ptParams = new Proxy(new URLSearchParams(window.location.search), {get: (searchParams, prop) => searchParams.get(prop),});
let post_type_value = ptParams.post_type;
let iso = $scroll_container.data('isotope');
if(iso) {
$scroll_container.infiniteScroll({
//path: 'page/{{#}}/?s=' + searchParam.search_term + '&post_type=' + post_type_value,
path: '.next-post-link',
append: '.scroll-post',
button: '.btn-scroll',
outlayer: iso,
loadOnScroll: false,
scrollThreshold: 300,
status: '.page-load-status',
hideNav: '.pagination'
}); //infinite scroll
jQuery('.btn-scroll').on('click', function () {
$scroll_container.on('load.infiniteScroll', function (event) {
$scroll_container.isotope('layout');
jQuery('.page-load-status').detach().appendTo(jQuery('.scroll-content'));
}); //on load function
}); //on click function
} //iso check
/*
* Filter
*/
let $ajax_container = jQuery('#ajax_container');
let s_term = jQuery('.search-field').val();
let s_cat_text = jQuery(".form-select :selected").text(); //not needed for query string
let s_cat_val = jQuery(".form-select :selected").val();
let s_order = jQuery("input[type='radio'][name='date']:checked").val();
let s_post_type = jQuery("input[type='hidden'][name='post_type']").val();
//ajax call
jQuery('#filter').submit(function(e) {
//jQuery('.btn-filter').on('click', function () {
e.preventDefault();
let filter = jQuery('#filter');
jQuery.ajax({
url: filter.attr('action'),
data: filter.serialize(), // form data
type : filter.attr('method'),
beforeSend : function(xhr) {
filter.find('.btn-filter').text('Filtering...');
console.log(filter.serialize());
},
success : function( data ) {
filter.find('.btn-filter').text('Apply filter');
let iso = $scroll_container.data('isotope');
let inf = $scroll_container.data('infniteScroll');
//remove initial results infinite scroll and isotope for ajax call
if(inf) {
$scroll_container.infiniteScroll('destroy');
}
if(iso) {
$scroll_container.isotope('destroy');
}
$scroll_container.remove(); //completely remove initial results
let iso_ajax = $ajax_container.data('isotope');
//let inf_ajax = $ajax_container.data('infniteScroll'); //does not work, reported to developer
//InfiniteScroll.data( element ); Infinite Scroll instance via its element does not work either
//if ajax infinite scroll and isotope exist already remove so reset
$ajax_container.infiniteScroll('destroy');
if(iso_ajax) {
$ajax_container.isotope('destroy');
$ajax_container.empty();
}
//create "new" isotope instance
$ajax_container.isotope({
layoutMode: 'fitRows',
itemSelector: '.scroll-post'
}); //isotope
let $data = $(data); // store data in jQuery object
$ajax_container.append($data);
$ajax_container.isotope('insert', $data );
let isoajax = $ajax_container.data('isotope');
//create "new" infinite scroll instance
$ajax_container.infiniteScroll({
path: '/page/{{#}}/?s=' + s_term + '&post_type=' + s_post_type + '&categoryfilter=' + s_cat_val + '&date=' + s_order,
append: '.scroll-post',
button: '.btn-scroll',
outlayer: isoajax,
loadOnScroll: false,
scrollThreshold: 300,
checkLastPage: true,
history: false,
status: '.page-load-status',
hideNav: '.pagination',
debug: true
}); //infinitescroll
jQuery('.btn-scroll').on('click', function () {
console.log('click ajax inside');
$ajax_container.on('load.infiniteScroll', function (event) {
console.log('infinite ajax inside');
$ajax_container.isotope('layout');
jQuery('.page-load-status').detach().appendTo(jQuery('#ajax_container'));
}); //on load function
}); // on click function
// reset filter
//filter[0].reset();
} // success
}); //ajax call
//return false; //e.preventDefault is used
}); //submit function
//Bind click event listener to the submit button
jQuery(document).on('click', 'button[type="submit"]', function() {
jQuery(this).parents('form').submit();
});
}); // window on load
Please excuse my indentation, thats it!
If you can optimize this code to make it better or come up with a it will be greatly appreciated.
example.com/wp-json/wp/v2/posts
, there's no need to use the old legacy AJAX API with a custom handler, just use the modern REST API that's already built in – Tom J Nowell ♦ Commented Feb 22, 2022 at 14:41