Almost built in-house AJAX filtering - but need some advice

Hello - I know there are already a few topics around AJAX filtering (one created by myself), but to my knowledge there is still no fully integrated working solution out there, and the demand for it is quite big.

After numerous attempts to create the code for it myself, I think I am close to achieving a working version (leveraging the master code hivepress/includes/blocks/class-listings.php at master · hivepress/hivepress · GitHub suggested by @ihor in another similar topic), but I need some help from the community.

Right now, when I click on an category filter, no front-end error message appears but also no listings are displayed (although there are existing listings respecting the filter). However, if I try to select an attribute filter, it gives the error message (hardcoded by me) “No listings found that match your criteria”. (again, despite listings existing).

My debug logs show that the WP_Query finds the posts, the loop runs, and the Listing object is found correctly. However, the render() method returns an empty string for every post. For example, here are the logs from one filter attempt:

  • Debug AJAX: Found 8 posts. Starting render loop.

    Debug AJAX: Loop iteration for Post ID: 649

    Debug AJAX: Successfully got Listing object for ID: 649

    Debug AJAX: Attempting to render template for listing ID: 649

    Debug AJAX: Render SUCCESSFUL but output is EMPTY for listing

This proves the listing-view-block template is failing silently during the wp_ajax_ call, most likely because a function or service used inside the template is not initialized. For context, I will also add the code in the comments.

I guess the next question that could hopefully solve this last bit is: What is the correct method to reliably query listings AND render the listing-view-block template during an AJAX request handled via admin-ajax.php? Standard methods seem to be failing due to these initialization issues.

Thanks a lot!

Code:

add_action( ‘wp_ajax_custom_listing_filter’, ‘custom_listing_filter_handler’ );
add_action( ‘wp_ajax_nopriv_custom_listing_filter’, ‘custom_listing_filter_handler’ ); // For non-logged-in users

function custom_listing_filter_handler() {
// Build the query arguments
$args = [
‘post_type’ => ‘hp_listing’,
‘posts_per_page’ => get_option( ‘posts_per_page’ ),
‘tax_query’ => [ ‘relation’ => ‘AND’ ],
‘meta_query’ => [ ‘relation’ => ‘AND’ ],
];

// Add category filter
if ( ! empty( $_POST['_category'] ) ) {
    $category_term_id = intval( $_POST['_category'] );
    if ($category_term_id > 0) {
        $args['tax_query'][] = [ 'taxonomy' => 'hp_listing_category', 'field' => 'term_id', 'terms' => $category_term_id ];
    }
}

// Add "brand" filter
if ( ! empty( $_POST['brand'] ) && is_array($_POST['brand']) ) {
    $brand_term_ids = array_map('intval', $_POST['brand']);
    $args['tax_query'][] = [ 'taxonomy' => 'hp_listing_attribute_brand', 'field' => 'term_id', 'terms' => $brand_term_ids, 'operator' => 'IN' ];
}

// Add price range fiter
if ( ! empty( $_POST['price'] ) && is_array( $_POST['price'] ) && count( $_POST['price'] ) == 2 ) {
    $min_price = floatval( $_POST['price'][0] );
    $max_price = floatval( $_POST['price'][1] );
    if ( $min_price >= 0 && $max_price > 0 && $max_price >= $min_price ) {
         $args['meta_query'][] = [ 'key' => 'hp_price', 'value' => [ $min_price, $max_price ], 'type' => 'NUMERIC', 'compare' => 'BETWEEN' ];
    }
}


// --- Run the query ---
$query = new WP_Query( $args );

// --- Render results using the direct Template instantiation method ---

if ( function_exists( ‘hivepress’ ) ) {
if ( $query->have_posts() ) {
error_log(‘Debug AJAX: Found ’ . $query->post_count . ’ posts. Starting render loop.’);
while ( $query->have_posts() ) {
$query->the_post();
$post_id = get_the_ID(); // Get post ID for logging

            error_log('Debug AJAX: Loop iteration for Post ID: ' . $post_id);

            // Get the HivePress Listing object
            $listing = \HivePress\Models\Listing::query()->get_by_id( $post_id );

            if ( $listing ) {
                 error_log('Debug AJAX: Successfully got Listing object for ID: ' . $post_id);

                // Try rendering the tempate
                try {
                    error_log('Debug AJAX: Attempting to render template for listing ID: ' . $post_id);

                    $template_output = ( new \HivePress\Blocks\Template(
                        [
                            'template' => 'listing/view/listing-view-block',
                            'context'  => [ 'listing' => $listing ],
                        ]
                    ) )->render();

                    if (empty($template_output)) {
                        error_log('Debug AJAX: Render SUCCESSFUL but output is EMPTY for listing ID: ' . $post_id);
                    } else {
                         error_log('Debug AJAX: Render SUCCESSFUL with output for listing ID: ' . $post_id);
                         echo $template_output; // Only echo if successful
                    }

                } catch ( \Throwable $e ) {
                    // Log any error/exception during rendering
                     error_log('Debug AJAX: ERROR during template render for listing ID: ' . $post_id . ' - Message: ' . $e->getMessage());
                }
                // --- End rendering attempt ---

            } else {
                 error_log('Debug AJAX: FAILED to get Listing object for post ID: ' . $post_id);
            }
        } // end while
         error_log('Debug AJAX: Finished render loop.');

    } else { // if no posts found
         error_log('Debug AJAX: No posts found by WP_Query.');
        echo '<p class="hp-no-results">No listings found that match your criteria.</p>';
    }
} else { // if hivepress() doesn't exist
     error_log('Debug AJAX: hivepress() function check failed.');
    echo '<p class="hp-no-results">Error: HivePress core function not found.</p>';
}

wp_reset_postdata();
error_log('--- AJAX Filter Request End ---');
wp_die();

}

/**

    1. PHP function to add the necessary JavaScript to the page footer.
      */
      function add_ajax_filter_script() {
      // Get the correct AJAX URL
      $ajax_url = admin_url( ‘admin-ajax.php’ );

    // The JavaScript code
    $script = "
    jQuery(document).ready(function($) {
    // Define the AJAX URL for JS
    const my_ajax_url = ‘" . esc_js( $ajax_url ) . "’;

     // Target the main filter form
     const filterForm = $('.hp-form--listing-filter');
     
    
     // Target the container where listings are displayed. 
     const resultsContainer = $('.hp-listings'); 
    
     // Ensure both elements exist before proceeding
     if (filterForm.length === 0 || resultsContainer.length === 0) {
         console.warn('AJAX Filter Error: Form or Results container not found.');
         return;
     }
    
     // Function to handle the AJAX request
     function handleAjaxFilter(formData) {
         resultsContainer.css('opacity', '0.5'); 
    
         $.ajax({
             type: 'POST',
             url: my_ajax_url, 
             data: formData + '&action=custom_listing_filter', // Send form data + our action hook
             success: function(response) {
                 // Replace the content and remove loading indicator
                 resultsContainer.html(response);
                 resultsContainer.css('opacity', '1'); 
                 
                 // Update the browser URL without reloading
                 const newUrl = window.location.pathname + '?' + formData;
                 history.pushState(null, '', newUrl);
             },
             error: function(jqXHR, textStatus, errorThrown) {
                 console.error('AJAX Filter Error:', textStatus, errorThrown);
                 resultsContainer.html('<p>An error occurred loading listings.</p>');
                 resultsContainer.css('opacity', '1'); 
             }
         });
     }
    
     // Trigger AJAX on any change in the filter form (checkbox, radio, select)
     filterForm.on('change', 'input, select', function() {
         // Small delay to allow multiple selections (like checkboxes) before firing
         setTimeout(() => {
             handleAjaxFilter(filterForm.serialize());
         }, 500); // 500ms delay
     });
    
     // Prevent the default form submission (page reload) if user hits Enter or clicks a submit button
     filterForm.on('submit', function(event) {
         event.preventDefault();
         handleAjaxFilter(filterForm.serialize());
     });
    

    });
    ";

    wp_add_inline_script( ‘hivepress-core-frontend’, $script );
    }
    add_action( ‘wp_enqueue_scripts’, ‘add_ajax_filter_script’, 1000 );

Hi,

There seems to be an issue with the template name, currently it’s like a path but it should be like:

$template_output = ( new \HivePress\Blocks\Template(
	[
		'template' => 'listing_view_block',

		'context'  => [
			'listing' => $listing,
		],
	]
) )->render();

We’ll also try to deliver this feature as soon as possible, as the possibility to render blocks separately within the same template was added to the framework for this exact purpose.

Hope this helps