SOLVED: Custom Query for Nav Menus

Hello Bricks Team,

I just create a custom query type to loop a menu item from menus, here is my current code

// Register a custom query type for menu items
add_filter('bricks/setup/control_options', function($control_options) {
    $control_options['queryTypes']['menu_items'] = esc_html__('Menu Items', 'bricks-child');
    return $control_options;
});
// Add custom control for selecting the menu
add_filter('bricks/elements/container/controls', 'add_menu_controls', 40);
add_filter('bricks/elements/block/controls', 'add_menu_controls', 40);
add_filter('bricks/elements/div/controls', 'add_menu_controls', 40);

function add_menu_controls($controls) {
    $menus = get_terms('nav_menu', ['hide_empty' => true]);
    $menu_options = [];

    if (!empty($menus) && !is_wp_error($menus)) {
        foreach ($menus as $menu) {
            $menu_options[$menu->slug] = $menu->name;
        }
    }

    $newControls['menuNameOrId'] = [
        'tab'         => 'content',
        'label'       => esc_html__('Select Menu', 'bricks-child'),
        'type'        => 'select',
        'options'     => $menu_options,
        'placeholder' => esc_html__('Select a menu...', 'bricks-child'),
        'required'    => [
            ['query.objectType', '=', 'menu_items'],
            ['hasLoop', '!=', false],
            ['isSubmenu', '!=', true]
        ],
    ];

    $newControls['isSubmenu'] = [
        'tab'         => 'content',
        'label'       => esc_html__('Is it for submenu?', 'bricks-child'),
        'type'        => 'checkbox',
        'required'    => [
            ['query.objectType', '=', 'menu_items'],
        ],
        'default'     => false,
    ];

    $query_key_index = absint(array_search('query', array_keys($controls)));
    $new_controls = array_slice($controls, 0, $query_key_index + 1, true) + $newControls + array_slice($controls, $query_key_index + 1, null, true);

    return $new_controls;
}
// Execute the custom query to fetch menu items and handle submenus
add_filter('bricks/query/run', function($results, $query_obj) {
    if ($query_obj->object_type !== 'menu_items') {
        return $results;
    }

    $settings = $query_obj->settings;
    if (!$settings['hasLoop']) {
        return [];
    }

    $menu_name_or_id = $settings['menuNameOrId'] ?? '';
    $is_submenu = $settings['isSubmenu'] ?? false;

    if (!empty($menu_name_or_id)) {
        $menu_items = wp_get_nav_menu_items($menu_name_or_id);

        if (!empty($menu_items)) {
            if (!$is_submenu) {
                foreach ($menu_items as $menu_item) {
                    if ($menu_item->menu_item_parent == 0) {
                        $results[] = $menu_item;
                    }
                }
            } else {
                $parent_menu_item = \Bricks\Query::get_loop_object();

                if ($parent_menu_item) {
                    $parent_id = $parent_menu_item->ID;

                    foreach ($menu_items as $menu_item) {
                        if ($menu_item->menu_item_parent == $parent_id) {
                            $results[] = $menu_item;
                        }
                    }
                }
            }
            return $results;
        }
    }

    return [];
}, 10, 2);
// Map the entire menu item object to the loop object
add_filter('bricks/query/loop_object', function($loop_object, $loop_key, $query_obj) {
    if ($query_obj->object_type !== 'menu_items') {
        return $loop_object;
    }

    if (isset($query_obj->results[$loop_key])) {
        $loop_object = $query_obj->results[$loop_key];
        $meta_data = get_post_meta($loop_object->ID);

        foreach ($meta_data as $key => $value) {
            if (strpos($key, '_') !== 0) {
                $acf_value = get_field($key, $loop_object->ID);
                if ($acf_value) {
                    $loop_object->$key = $acf_value;
                }
            }

            if (is_array($value) && count($value) === 1) {
                $value = $value[0];
            }

            if (!isset($loop_object->$key)) {
                $loop_object->$key = $value;
            }
        }
    }

    return $loop_object;
}, 10, 3);
// Fetch specific data from the current loop object
function get_menu_item_data($key) {
    $menu_item = \Bricks\Query::get_loop_object();

    if (!$menu_item) {
        return '';
    }

    switch ($key) {
        case 'title':
            return esc_html($menu_item->title ?? '');

        case 'url':
            return esc_url($menu_item->url ?? '#');

        case 'is_submenu':
            return !empty($menu_item->menu_item_parent);

        case 'parent_id':
            return $menu_item->menu_item_parent ?? 0;

        default:
            return '';
    }
}

// Register the echo function
add_filter('bricks/code/echo_function_names', function() {
    return ['get_menu_item_data'];
});

at the moment, it works perfectly render the top level menu, see screenshot below

but, i got an issue when rendering the submenu item (using nested loop)

the sub menu is never rendered, the issue is the code in bricks/query/run filter at the $submenu is true condition is never triggered, i’ve tried add an error_log to see what is happens but it’s never rendered

if (!$is_submenu) {
    foreach ($menu_items as $menu_item) {
        if ($menu_item->menu_item_parent == 0) {
            $results[] = $menu_item;
        }
    }
} else { //This condition is never triggered
    $parent_menu_item = \Bricks\Query::get_loop_object();

    if ($parent_menu_item) {
        $parent_id = $parent_menu_item->ID;

        foreach ($menu_items as $menu_item) {
            if ($menu_item->menu_item_parent == $parent_id) {
                $results[] = $menu_item;
            }
        }
    }
}

I wonder if I made a mistake at the code, thanks

cc: @itchycode

2 Likes

I seem to have found the root of the problem, the issue is coming from these check

if (!empty($menu_name_or_id))
if (!empty($menu_items))

the control for isSubmenu checkbox is actually prevent to use the menuNameOrId because the purposes is use the same menu from the parent query loop, so the above condition will return false because it has no menu selected.

my current solution is create a variable

static $stored_menu_id = null;

this variable will filled inside top-level menu query logic

$menu_id = $settings['menuNameOrId'] ?? '';
$is_submenu = $settings['isSubmenu'] ?? false;

// Static variable to store top-level menu ID and query ID
static $stored_menu_id = null;

if (!$is_submenu) {
    $stored_menu_id = $menu_id; //The menu is stored here
    $menu_items = wp_get_nav_menu_items($menu_id);

    foreach ($menu_items as $item) {
        if ($item->menu_item_parent == 0) {
            $results[] = $item;
        }
    }
}

then inside the sub-menu query logic, we can use that variable to loop the menu-items for sub-menu, here is the full code

add_filter('bricks/query/run', function($results, $query_obj) {
    if ($query_obj->object_type !== 'menu_items') {
        return $results;
    }

    $settings = $query_obj->settings;
    if (!$settings['hasLoop']) {
        return [];
    }

    $menu_id = $settings['menuNameOrId'] ?? '';
    $is_submenu = $settings['isSubmenu'] ?? false;

    // Static variable to store top-level menu ID and query ID
    static $stored_menu_id = null;

    if (!$is_submenu) {
        $stored_menu_id = $menu_id;
        $menu_items = wp_get_nav_menu_items($menu_id);

        foreach ($menu_items as $item) {
            if ($item->menu_item_parent == 0) {
                $results[] = $item;
            }
        }
    } else {
        if ($stored_menu_id) {
            $menu_items = wp_get_nav_menu_items($stored_menu_id);
            $parent_query_id = get_query_id(1);
            $parent_item = \Bricks\Query::get_loop_object($parent_query_id);

            if ($parent_item) {
                $parent_id = $parent_item->ID;

                foreach ($menu_items as $item) {
                    if ($item->menu_item_parent == $parent_id) {
                        $results[] = $item;
                    }
                }
            }
        }
    }

    return $results;
}, 10, 2);

as you can see above, we also need a query id of the top-level menu to fetch a loop object off all menus (top-level and sub-menu), so in this moment i’m using a function from @itchycode to get the query id of top-level menu using \Bricks\Query::is_any_looping() you can see the detail here Bricks Builder Useful Functions And Tips and i just change the function name to make it more simple

I hope this helps everyone who comes here.

4 Likes

Hey there,
Thanks for sharing this! I was wondering why you didn’t use the nav nestable for this?
I am looking into getting something like this going as well, but where the menu gets pulled from a dynamic context variable (like the slug).
peace

hi @tho

the reason i’m not using nav nestable element is because i think that’s just a preset element to build a static menu, but with some accessibility settings included in it.
The solution above is for make the menu keep dynamic from native WP menus (effective for end users), but coming with freedom to add anything inside your nav.

By the way, i also noticed Bricksextras soon will have the same thing for pulled the menu item as a query loop, try to check this out

Thanks for the link to BricksExtras. I did check this out a few times already but would rather code a solution myself instead of pulling in a massiv library of elements that I don’t need.

Maybe it is not such a big thing to replace the select box, from your solution, with a dynamic field that constructs the menus name? I might give that a try.

I check your solution as well but could not get the submenu to work. It stops the output at the first submenu item. Did you have this before?

Greetings

@ivan_nugraha has made a youtube video about explaining how to use the code, please take a look on it