I am writing a product filter, and ran into this issue.
I have 2 different attributes of my product filter: Tilt -- as a checkbox with "Yes" or "No", and Multibrand Compatible -- as a checkbox with Yes or No. If I click Multibrand Compatible "No", the products without the attribute of Multibrand Compatible no are displayed.
Let's say initially I click on Multibrand with the attribute of "No". If I then click on Tilt with the attribute of "Yes", the products that have a tilt, and are NOT Multibrand Compatible should be displayed.
Instead, I get all of the products with a tilt, including ones with a positive Multibrand Compatibility.
The URL Path is:
/?multibrand=no&tilt=yes
Here is my HTML:
function multiplan_compatible_filter() {
// Start output buffering to capture the HTML output
ob_start();
?>
<!-- HTML structure for the Multibrand filter dropdown -->
<div id="multibrand-tilt-filter-container" class="multibrand-dropdown">
<div class="multibrand-dropdown-wrapper">
<button class="multibrand-dropdown-btn">Multibrand Compatible ▼</button>
<div class="multibrand-dropdown-content">
<label>
<input type="checkbox" id="multibrand-yes" value="yes" data-attribute="pa_multibrand-compatible">
Yes
</label>
<label>
<input type="checkbox" id="multibrand-no" value="no" data-attribute="pa_multibrand-compatible">
No
</label>
</div>
</div>
</div>
Here is the main Javascript and AJAX:
<!-- JavaScript for handling the filter functionality -->
<script>
jQuery(function($) {
// Toggle dropdown visibility when the button is clicked
$('.multibrand-dropdown-btn').click(function(e) {
e.stopPropagation();
$('.multibrand-dropdown-content').toggleClass('show');
});
// Close dropdown when clicking outside
$(window).click(function() {
$('.multibrand-dropdown-content, .tilt-dropdown-content').removeClass('show');
});
// Handle checkbox changes
$('#multibrand-yes, #multibrand-no').on('change', function() {
const currentCheckbox = $(this);
const isChecked = currentCheckbox.is(':checked');
const isYesFilter = currentCheckbox.attr('id') === 'multibrand-yes';
// Update URL parameter
let currentUrl = new URL(window.location.href);
let params = currentUrl.searchParams;
params.delete('multibrand');
if (isChecked) {
params.set('multibrand', isYesFilter ? 'yes' : 'no');
}
history.pushState(null, '', '?' + params.toString());
// Ensure only one checkbox is checked at a time
if (isYesFilter) {
$('#multibrand-no').prop('checked', false);
} else {
$('#multibrand-yes').prop('checked', false);
}
// Show loading overlay
$('body').block({
message: 'Updating products...',
overlayCSS: { background: '#fff', opacity: 0.6 }
});
// Make AJAX request to update products
$.ajax({
url: wc_add_to_cart_params.ajax_url,
type: 'POST',
data: {
action: 'multibrand_compatible_products',
filter_yes: isYesFilter && isChecked,
filter_no: !isYesFilter && isChecked
},
success: function(response) {
if (response.success) {
// Update product display
$('.products').html(response.data.filtered_products);
if (response.data.other_products) {
$('.products').append('<h2>Other Products</h2>' + response.data.other_products);
}
}
$('body').unblock();
},
error: function() {
console.error('Failed to update products');
$('body').unblock();
},
});
});
// Initialize state on page load
$(document).ready(function() {
let params = new URLSearchParams(window.location.search);
let multibrandValue = params.get('multibrand');
// Reset checkboxes
$('#multibrand-yes, #multibrand-no').prop('checked', false);
// Set initial state based on URL parameter
if (multibrandValue === 'yes') {
$('#multibrand-yes').prop('checked', true);
} else if (multibrandValue === 'no') {
$('#multibrand-no').prop('checked', true);
}
// Trigger initial filter if needed
if (multibrandValue) {
$('#multibrand-yes, #multibrand-no').first(':checked').trigger('change');
}
});
});
</script>
<?php
// Return the captured HTML output
return ob_get_clean();
}
// Register AJAX actions for both logged-in and non-logged-in users
add_action('wp_ajax_multibrand_compatible_products', 'multibrand_compatible_products');
add_action('wp_ajax_nopriv_multibrand_compatible_products', 'multibrand_compatible_products');
// AJAX handler for filtering products
function multibrand_compatible_products() {
// Validate incoming data
if (!isset($_POST['filter_yes']) || !isset($_POST['filter_no'])) {
wp_send_json_error();
return;
}
$filter_yes = $_POST['filter_yes'] === 'true';
$filter_no = $_POST['filter_no'] === 'true';
// Include the unified filter function
require_once get_template_directory() . '/functions.php_FILES/Filter/unified_filter.php';
// Apply the filter and get results
$result = unified_product_filter('pa_multibrand-compatible', $filter_yes, $filter_no);
// Send the filtered results back as a JSON response
wp_send_json_success($result);
}
I made a unified Query System for the rest of my code so it is easier to scale:
<?php
function unified_product_filter($attribute, $filter_yes, $filter_no) {
// Set up the base arguments for WP_Query to fetch products
$filtered_args = array(
'post_type' => 'product',
'posts_per_page' => -1, // Retrieve all matching products
);
// Initialize the tax_query array for filtering by product attributes
$filtered_args['tax_query'] = array(
'relation' => 'AND', // All conditions in this array must be met
);
// Add attribute filters based on user selection
if ($filter_yes) {
// If 'Yes' is selected, add a filter for products with 'yes' value for the given attribute
$filtered_args['tax_query'][] = array(
'taxonomy' => $attribute, // The product attribute to filter by (e.g., 'pa_multibrand-compatible')
'field' => 'slug',
'terms' => 'yes',
);
} elseif ($filter_no) {
// If 'No' is selected, add a filter for products with 'no' value for the given attribute
$filtered_args['tax_query'][] = array(
'taxonomy' => $attribute,
'field' => 'slug',
'terms' => 'no',
);
}
// Perform the query to get filtered products
$filtered_query = new WP_Query($filtered_args);
// Initialize variables to store HTML output
$filtered_products_html = '';
$other_products_html = '';
// Check if there are filtered products
if ($filtered_query->have_posts()) {
while ($filtered_query->have_posts()) {
$filtered_query->the_post();
// Generate HTML for each filtered product
$filtered_products_html .= generate_product_html(get_the_ID());
}
wp_reset_postdata(); // Reset the global $post variable
}
// If no products match the filter, get all products
if (empty($filtered_products_html)) {
$all_products_query = new WP_Query(array(
'post_type' => 'product',
'posts_per_page' => -1,
));
if ($all_products_query->have_posts()) {
while ($all_products_query->have_posts()) {
$all_products_query->the_post();
// Generate HTML for each product
$other_products_html .= generate_product_html(get_the_ID());
}
wp_reset_postdata();
}
}
// Return the results as an array
return array(
'filtered_products' => $filtered_products_html,
'other_products' => $other_products_html,
);
}
// Helper function to generate HTML for a single product
function generate_product_html($product_id) {
$product = wc_get_product($product_id);
// Generate HTML for the product (customize as needed)
$html = '<div class="product">';
$html .= '<h3>' . $product->get_name() . '</h3>';
$html .= '<p>Price: ' . $product->get_price_html() . '</p>';
// Add more product details as needed
$html .= '</div>';
return $html;
}
I believe that anytime a new filter is added, whether through a click of a checkbox, or a selection of a dropdown, the on-loading query either over-writes everything else or only does a query for the toggled newest filter. Is there any way to have the filtering work on-page load for each seperate URL path of the filtered attribute? (ie: tilt=yes and multibrand=no display products with tilt of "yes" and multibrand of "no")