How to loop through (and filter) ACF repeater field?

The goal: Create a loop of reviews, excluding those where “has_written_review” is false.

Setup: I’m using ACF with a custom post type called “person”. Within this post type, there’s an ACF repeater field called “reviews”. This repeater field contains the subfields: “star_rating”, “has_written_review” (true/false), and “written_review”.

I’ve created a Bricks template for the custom post type “person”. In this template, there’s a loop set up for the “reviews” repeater.

I want to display the reviews that have a written review, skipping those that only have a star rating. Typically, I believe this would be managed by adding a meta query to the loop, referencing the “has_written_review” boolean field. However, when selecting the ACF relationship field (instead of “post”) for the loop, these additional options are not available.

Aside from avoiding outputting empty divs, if the loop accurately reflects the number of reviews with a written component, I could use query_results_count to conditionally show a “Reviews” heading.

What is the recommended/correct way to achieve this in Bricks?

I’m aware the visual result could be achieved with some CSS selectors or creating a custom PHP function that returns the count I’m looking for, but I’d like to learn the correct way to make and filter an ACF repeater loop in Bricks.

Hey Brandon,

you could use ACF’s format_value hook:

add_filter( 'acf/format_value/name=reviews', function( $value ) {
    return array_filter( $value, function( $review ) {
        return $review['has_written_review'] === true;
    } );
} );

Let me know if that helps.

Best,

André

1 Like

Thanks for the suggestion Andre. Good to learn this is an option. Can the filter be applied selectively (for a specific loop), rather than globally?

On Facebook @Sridhar pointed me towards Brick’s bricks/query/run filter: Filter: bricks/query/run – Bricks Academy

I’m not sure how “correct” my resulting code is, but it does function as desired:

// Create custom loop to get profile reviews where "written_review" = true
// ***********************************
/* Add new query type control to query options */
add_filter( 'bricks/setup/control_options', 'profile_reviews_query_controls');
function profile_reviews_query_controls( $control_options ) {
    $control_options['queryTypes']['profile_reviews_query'] = esc_html__( 'Profile Reviews Query', 'your-text-domain' );
    return $control_options;
}

/* Run new query if option selected */
add_filter( 'bricks/query/run', 'run_profile_reviews_query', 10, 2);
function run_profile_reviews_query( $results, $query_obj ) {
    if ( $query_obj->object_type !== 'profile_reviews_query' ) {
        return $results;
    }
    
    $results = get_filtered_reviews();
    return $results;
}

/* Setup loop object for reviews */
add_filter( 'bricks/query/loop_object', 'setup_profile_review_data', 10, 3);
function setup_profile_review_data( $loop_object, $loop_key, $query_obj ) {
    if ( $query_obj->object_type !== 'profile_reviews_query' ) {
        return $loop_object;
    }
    
    return $loop_object;
}

/* Return results from our profile reviews query */
function get_filtered_reviews() {
    $filtered_reviews = [];
    
    // Get the current post ID
    $post_id = get_the_ID();
    
    // Get the reviews repeater field
    if( have_rows('reviews', $post_id) ) {
        while( have_rows('reviews', $post_id) ) {
            the_row();
            
            $written_review = get_sub_field('written_review');
            
            // Only include reviews where written_review is true
            if( $written_review ) {
                $filtered_reviews[] = [
                    'star_rating' => get_sub_field('star_rating'),
                    'review' => get_sub_field('review'),
                    'name' => get_sub_field('name'),
                ];
            }
        }
    }
    
    return $filtered_reviews;
}

/* Add profile reviews dynamic data tags */
add_filter( 'bricks/dynamic_tags_list', 'add_profile_review_tags' );
function add_profile_review_tags( $tags ) {
    $tags[] = [
        'name'  => '{cf_reviews_star_rating}',
        'label' => 'Review Star Rating',
        'group' => 'Profile Reviews Data',
    ];
    $tags[] = [
        'name'  => '{cf_reviews_name}',
        'label' => 'Review Name',
        'group' => 'Profile Reviews Data',
    ];
    $tags[] = [
        'name'  => '{cf_reviews_review}',
        'label' => 'Review Text',
        'group' => 'Profile Reviews Data',
    ];
    return $tags;
}

/* Render profile reviews dynamic data tags */
add_filter( 'bricks/dynamic_data/render_tag', 'render_profile_review_tags', 10, 3 );
function render_profile_review_tags( $tag, $post, $context = 'text' ) {
    $loop_object = Bricks\Query::get_loop_object();
    
    if ( !is_array($loop_object) ) {
        return $tag;
    }
    
    switch ( $tag ) {
        case 'cf_reviews_star_rating':
            return isset($loop_object['star_rating']) ? $loop_object['star_rating'] : '';
        case 'cf_reviews_name':
            return isset($loop_object['name']) ? $loop_object['name'] : '';
        case 'cf_reviews_review':
            return isset($loop_object['review']) ? $loop_object['review'] : '';
        default:
            return $tag;
    }
}

/* Render profile reviews dynamic data tags in content */
add_filter( 'bricks/dynamic_data/render_content', 'render_profile_review_content', 10, 3 );
add_filter( 'bricks/frontend/render_data', 'render_profile_review_content', 10, 2 );
function render_profile_review_content( $content, $post, $context = 'text' ) {
    $loop_object = Bricks\Query::get_loop_object();
    
    if ( !is_array($loop_object) ) {
        return $content;
    }
    
    $tags = [
        '{cf_reviews_star_rating}' => isset($loop_object['star_rating']) ? $loop_object['star_rating'] : '',
        '{cf_reviews_name}' => isset($loop_object['name']) ? $loop_object['name'] : '',
        '{cf_reviews_review}' => isset($loop_object['review']) ? $loop_object['review'] : '',
    ];
    
    return str_replace(array_keys($tags), array_values($tags), $content);
}

Hey Brandon,

don’t know your exact setup but something like this could work:

add_filter( 'acf/format_value/name=reviews', function( $value ) {
    $element_id = \Bricks\Query::get_query_element_id();
    if ( $element_id !== 'LOOP_ELEMENT_ID_HERE' ) {
        return $value;
    }
    
    return array_filter( $value, function( $review ) {
        return $review['has_written_review'] === true;
    } );
} );

Best,

André

That would be fantastic. I tried to implement this but wasn’t successful. The get_query_element_id() doesn’t return the ID of the loop, it might be the ID of the page/post. I can’t figure out how to use this approach and only trigger the filter for a specific Bricks loop… shame because that’s much cleaner than the solution I’m using right now!

Hey Brandon,

are you using the repeater field multiple times on the same page / template? Or in different places? And where do you want to filter it and where not? Can you describe your setup a bit more?

Best,

André

Yes, multiple times in the same template. I have a single post template for a custom post type. Near the top there’s a place where the average star rating is shown and in parenthesis is the total number of reviews this average is based on (i.e. ★★★★ (14)). At the bottom of the page it shows all the reviews that include a written component. I’m using the loop counts to apply visibility conditions on the related elements (i.e. show if > 0).

Hey André

Trying to sort this myself too, needed it a few times actually. Using a filter inside an ACF repeater loop basically.

I am using:

add_filter( 'acf/format_value/name=reviews', function( $value ) {
    $element_id = \Bricks\Query::get_query_element_id();
    if ( $element_id !== '42c822' ) {
        return $value;
    }
    
    return array_filter( $value, function( $review ) {
        return $review['team_member_show_on_cta'] === true;
    } );
} );

But it’s not working :frowning: even when the id has brxe- preceeding it too. Any ideas at all? :smiley:

add_filter('acf/format_value/name=team_member_repeater', function ($value, $post_id, $field) {
    // Ensure the value is an array (repeater data)
    if (is_array($value)) {
        // Filter the repeater rows based on the specific ACF subfield 'team_member_show_on_cta' being true
        $filtered = array_filter($value, function ($row) {
            // Check if the subfield 'team_member_show_on_cta' is true
            return !empty($row['team_member_show_on_cta']); // Ensure this subfield name matches exactly
        });

        // Limit the number of filtered results to 5
        $limited = array_slice($filtered, 0, 3);
    } else {
        $limited = $value; // Return unfiltered if not an array
    }

    // Return the limited and filtered repeater data
    return $limited;
}, 10, 3);

After much trying, I finally got it to work with the above! Just change the repeater name in the filter itself to your repeater (name=) and the field to check/filter futher down - and voila!