DEV Community

Cover image for Sorting a Bricks query loop by a custom field chosen in a JetSmartFilters radio filter
Muazzam
Muazzam

Posted on Edited on

Sorting a Bricks query loop by a custom field chosen in a JetSmartFilters radio filter

I needed a sidebar radio list of custom field names (for example "Weight", "Length", "Rating") on a listing built with Bricks Builder, JetEngine and JetSmartFilters (JSF). When a user picks one, the listing should only show posts that have a value for that field, sorted highest to lowest by it, and any other active filters (taxonomy, search) should keep working alongside it.

JSF has a Sort widget and a Radio filter widget, but they work independently. Picking a radio value doesn't trigger a sort, and there's no setting for "when this value is selected, sort by that meta key". So I used the radio filter only to pass a plain value to the server, and changed the query in PHP through Bricks' bricks/posts/query_vars filter. No JavaScript needed.

The setup: a Bricks query loop with a JSF Query ID, a JetEngine CPT whose custom fields are all numeric, and a JSF Radio filter next to the existing taxonomy filters on the same page.

The radio filter

In JSF, create a Radio filter:

  • Data Source: Manual Input
  • Options List: one entry per field. Label is the display name, Value is the meta key.
Label: Weight       Value: weight
Label: Length       Value: length
Label: Rating       Value: rating
Enter fullscreen mode Exit fullscreen mode
  • This filter for: Bricks query loop
  • Query ID: the same ID set on your Bricks loop element (I used my-listing)
  • Apply on: Value change
  • Query Variable: _plain_query::sort_by_field

The _plain_query:: prefix tells JSF to pass the value through as a plain query variable instead of trying to build its own meta query from it.

On the Bricks side, select the query loop element and set the Query ID in the JSF settings panel (JSF adds the field once the element is marked as filterable). Every JSF filter on the page needs the same ID in "This filter for".

The hook

This goes in the theme's functions.php or a site-specific plugin.

add_filter('bricks/posts/query_vars', function($query_vars, $settings, $element_id, $element_name) {

    // Target only your specific Bricks loop by its JSF Query ID
    // Note: Bricks stores this under 'jsfb_query_id', not 'query_id'
    $query_id = $settings['jsfb_query_id'] ?? '';
    if ($query_id !== 'my-listing') return $query_vars;

    // JSF sends filter data via POST during AJAX requests
    // The _plain_query:: prefix becomes _plain_query_ in the POST key
    $query_data = $_POST['query'] ?? [];
    $sort_field = sanitize_key($query_data['_plain_query_sort_by_field'] ?? '');

    if (empty($sort_field)) return $query_vars;

    // Whitelist allowed field keys, never trust user input directly
    $allowed_fields = ['weight', 'length', 'rating']; // add all your fields
    if (!in_array($sort_field, $allowed_fields, true)) return $query_vars;

    // Filter: only show posts that have a non-empty value for this field
    $query_vars['meta_query'] = [
        'relation' => 'AND',
        [
            'key'     => $sort_field,
            'compare' => 'EXISTS',
        ],
        [
            'key'     => $sort_field,
            'value'   => '',
            'compare' => '!=',
        ],
    ];

    // Sort: numeric descending by the selected field
    $query_vars['orderby']  = 'meta_value_num';
    $query_vars['order']    = 'DESC';
    $query_vars['meta_key'] = $sort_field;

    return $query_vars;

}, 99, 4); // Priority 99 matters, see below


// Register the query var so WordPress doesn't strip it
add_filter('query_vars', function($vars) {
    $vars[] = 'sort_by_field';
    return $vars;
});
Enter fullscreen mode Exit fullscreen mode

The whitelist is not optional. The meta key comes from the request, and without the check anyone could sort or filter your listing by any meta key in the database.

What made it fail first

The code above is short. Getting there took longer, and every failure had the same shape: the hook ran fine and did nothing, with no error anywhere.

The settings key is jsfb_query_id

Inside the hook, Bricks stores the JSF Query ID under jsfb_query_id, not query_id. Checking query_id always returns empty, so the hook skips every request. I only found it by dumping the whole settings array:

add_filter('bricks/posts/query_vars', function($query_vars, $settings, $element_id, $element_name) {
    error_log(print_r($settings, true));
    return $query_vars;
}, 10, 4);
Enter fullscreen mode Exit fullscreen mode

Filter values arrive in POST

When a user clicks a filter, JSF sends an AJAX request and the values are in $_POST['query']. Reading $_GET gives you nothing on filter interactions.

The prefix changes the key name

With the Query Variable set to _plain_query::sort_by_field, the key in the POST payload is _plain_query_sort_by_field. The :: becomes _.

JSF overwrites you at default priority

This was the most painful one. At the default priority of 10, JSF runs its own bricks/posts/query_vars callback after mine and resets orderby back to date, so the code looks correct and the order is still wrong. Registering at 99 makes my hook run last, so it has the final say on the query arguments.

Also declare all four parameters ($query_vars, $settings, $element_id, $element_name) and pass 4 as the last argument to add_filter. $element_name was added in Bricks 1.11.1.

The full flow

User clicks radio → "Weight"
        ↓
JSF fires AJAX: POST query[_plain_query_sort_by_field] = weight
        ↓
bricks/posts/query_vars hook fires (priority 99)
        ↓
Hook reads _plain_query_sort_by_field from $_POST['query']
Validates against whitelist
Injects meta_query (EXISTS + not empty)
Injects orderby=meta_value_num, order=DESC, meta_key=weight
        ↓
Bricks executes WP_Query with modified args
        ↓
Results: only posts WITH a weight value, sorted highest → lowest
Other active filters (taxonomy, search) remain stacked, unaffected
Enter fullscreen mode Exit fullscreen mode

Debugging it

When this kind of integration does nothing, I log each step so I can see exactly where the chain breaks instead of guessing:

add_filter('bricks/posts/query_vars', function($query_vars, $settings, $element_id, $element_name) {

    error_log('HOOK FIRED, element: ' . $element_id);
    error_log('jsfb_query_id: ' . ($settings['jsfb_query_id'] ?? 'NOT SET'));

    $query_data = $_POST['query'] ?? [];
    error_log('POST query: ' . print_r($query_data, true));

    $sort_field = sanitize_key($query_data['_plain_query_sort_by_field'] ?? '');
    error_log('sort_field extracted: ' . ($sort_field ?: 'EMPTY'));

    // ... rest of your logic

    error_log('Final query_vars: ' . print_r($query_vars, true));

    return $query_vars;

}, 99, 4);
Enter fullscreen mode Exit fullscreen mode

With WP_DEBUG_LOG enabled the output goes to /wp-content/debug.log. Check it after each filter click. If "HOOK FIRED" shows up but jsfb_query_id is empty, it's the key name. If the final query_vars look right but the order is still wrong, something is running after you, which in my case was JSF at a later priority.

Top comments (0)