Filtering Query Loop of ACF Repeater field using WP Grid Builder

Hi,

I need assistance in using Bricks’ query loops and filter block.

I’m using WP Grid Builder and ACF in Brick’s theme page builder to try and filter custom post fields inside WooCommerce Products.

I have a query displaying the products (product query), then another query loop inside the first to display information from ACF repeater (repeater query).


image
image

I have a facet within WP Grid Builder targeting the necessary field within the ACF Repeater.

For some reason, inside the Bricks page builder, I can only get the facet to successfully filter against the product query, but not the repeater query. When I target the repeater query, the facet filter cannot see any of the values, so the range returns as 0.

image
image

Could anyone assist or offer suggestions to get the facet filter to apply to the repeater query as intended?

I ended up using ChatGPT to build a custom encoded solution.

Documenting it here incase someone else has similar issues.

In the functions.php of Bricks Child theme I exposed the product variant’s attributes to new meta keys, as well as flattening the attributes for the parent product:

/*Expose product_variation*/
// Expose variation meta fields in Bricks dynamic tags
add_filter('bricks/setup/control-options', function($options, $control) {
    if ($control['id'] === 'post.meta') {
        $meta_key = $control['options']['key'] ?? '';

        // List of allowed meta fields to expose
        $allowed_keys = [
            '_sku',
            '_price',
            'volume_litre',
            'power_output',
            'weight_kg',
            'shelves',
            'ext_width',
            'ext_depth',
            'ext_height',
            'int_width',
            'int_depth',
            'int_height',
        ];

        if (in_array($meta_key, $allowed_keys)) {
            global $post;
            if ($post && $post->post_type === 'product_variation') {
                $options['value'] = get_post_meta($post->ID, $meta_key, true);
            }
        }
    }
    return $options;
}, 10, 2);
// Optional: Make sure product_variation is queryable by Bricks
add_action('init', function() {
    global $wp_post_types;

    if (isset($wp_post_types['product_variation'])) {
        $wp_post_types['product_variation']->public = true;
        $wp_post_types['product_variation']->show_ui = true;
        $wp_post_types['product_variation']->show_in_menu = true;
        $wp_post_types['product_variation']->show_in_rest = true;
        $wp_post_types['product_variation']->exclude_from_search = false;
        $wp_post_types['product_variation']->has_archive = true;
    }
}, 20);

/*Add Flattened Variation Data to Parent Product*/
add_action('save_post_product', function($post_id) {
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;

    $product = wc_get_product($post_id);
    if (! $product || ! $product->is_type('variable')) return;

    $variation_ids = $product->get_children();
    $volumes_raw = [];
    $volumes_labelled = [];

    foreach ($variation_ids as $variation_id) {
        $volume = get_post_meta($variation_id, 'attribute_pa_volume', true);
        if ($volume && is_numeric($volume)) {
            $volumes_raw[] = $volume;
            $volumes_labelled[] = $volume . 'L';
        }
    }

    $volumes_raw = array_unique($volumes_raw);
    $volumes_labelled = array_unique($volumes_labelled);

    update_post_meta($post_id, 'all_variant_volumes', implode(', ', $volumes_labelled));
    update_post_meta($post_id, 'all_variant_volumes_raw', implode(', ', $volumes_raw));
});

Now that I have the variant’s attributes exposed, I moved away from using ACF Repeater.
For the interior loop I am using custom php.

return [
  'post_type'      => 'product_variation',
  'post_status'    => 'publish',
  'posts_per_page' => -1,
  'post_parent'    => get_the_ID(), // Works inside Bricks nested loops
  'meta_key'       => 'attribute_pa_volume', // The meta field to order by
  'orderby'        => 'meta_value_num',      // Numeric ordering
  'order'          => 'ASC',                 // Change to 'DESC' for reverse order
];

And a custom code block to house the element, which is built with php to pull the exposed attributes.

<?php
$volume     = get_post_meta(get_the_ID(), 'attribute_pa_volume', true);
$sku        = get_post_meta(get_the_ID(), '_sku', true);
$price      = get_post_meta(get_the_ID(), '_price', true);
$price      = is_numeric($price) ? $price : ''; // Ensure safe value
$parent_id  = wp_get_post_parent_id(get_the_ID());
$parent_url = get_permalink($parent_id);

// Gather all filterable attributes
$attrs = [
  'power_output'     => get_post_meta(get_the_ID(), 'attribute_pa_power-output', true),
  'weight'           => get_post_meta(get_the_ID(), 'attribute_pa_weight', true),
  'shelves'          => get_post_meta(get_the_ID(), 'attribute_pa_shelves', true),
  'external_width'   => get_post_meta(get_the_ID(), 'attribute_pa_external-width', true),
  'external_depth'   => get_post_meta(get_the_ID(), 'attribute_pa_external-depth', true),
  'external_height'  => get_post_meta(get_the_ID(), 'attribute_pa_external-height', true),
  'internal_width'   => get_post_meta(get_the_ID(), 'attribute_pa_internal-width', true),
  'internal_depth'   => get_post_meta(get_the_ID(), 'attribute_pa_internal-depth', true),
  'internal_height'  => get_post_meta(get_the_ID(), 'attribute_pa_internal-height', true),
  'price'            => $price,
];

if ($volume !== '' && $sku && $parent_url) {
    echo '<div class="variant-item"';
    echo ' data-volume="' . esc_attr($volume) . '"';
    foreach ($attrs as $key => $val) {
        echo ' data-' . esc_attr($key) . '="' . esc_attr($val) . '"';
    }
    echo '>';
    echo '<a href="' . esc_url($parent_url . '#' . $sku) . '">' . esc_html($volume) . 'L</a>';
    echo '</div>';
}
?>

image

I am still using WP Grid Builder to filter the parent products.

I’m then using JS, housed in Body (footer), to check the value applied to the WPGB filter facet then hide the variant’s elements that sit outside of the range.

<script>
function parseRangeFromURL(param) {
  const params = new URLSearchParams(window.location.search);
  const raw = params.get(param);
  if (!raw) return [NaN, NaN];
  const [min, max] = raw.split(',').map(Number);
  return [min, max];
}

function parseRangeFromFacetDOM(facetName) {
  const sliders = document.querySelectorAll(`input.wpgb-range[name="${facetName}[]"]`);
  if (sliders.length < 2) return [NaN, NaN];
  return [
    parseFloat(sliders[0].value),
    parseFloat(sliders[1].value),
  ];
}

function getActiveFilters() {
  const filters = {};

  const keys = [
    'volume', 'power_output', 'weight', 'shelves',
    'external_width', 'external_depth', 'external_height',
    'internal_width', 'internal_depth', 'internal_height',
    'price',
  ];

  keys.forEach(key => {
    let [min, max] = parseRangeFromURL(`_${key}`);
    if (isNaN(min) || isNaN(max)) {
      [min, max] = parseRangeFromFacetDOM(key);
    }
    filters[key] = [min, max];
  });

  return filters;
}

function filterVariants() {
  const filters = getActiveFilters();

  document.querySelectorAll('.variant-item').forEach(variant => {
    let visible = true;

    for (const key in filters) {
      const val = parseFloat(variant.dataset[key]);
      const [min, max] = filters[key];
      if (!isNaN(min) && !isNaN(max)) {
        if (isNaN(val) || val < min || val > max) {
          visible = false;
          break;
        }
      }
    }

    variant.style.display = visible ? '' : 'none';
  });

  document.querySelectorAll('.product-card').forEach(card => {
    const visibleVariants = card.querySelectorAll('.variant-item:not([style*="display: none"])').length;
    card.style.display = visibleVariants > 0 ? '' : 'none';
  });
}

function observeDomUpdates() {
  const observer = new MutationObserver(() => {
    setTimeout(filterVariants, 0); // Wait for DOM to rebuild
  });
  observer.observe(document.body, { childList: true, subtree: true });
}

document.addEventListener('DOMContentLoaded', () => {
  filterVariants();
  observeDomUpdates();
});
</script>

I believe this is everything I’m using to forcibly solve the problem.

ChatGPT pulled a lot of the weight here, but I wouldn’t have gotten anywhere close to this on my own.