Here’s what I wanted to do: give clients the ability to add conditional items to menus and be clear about which users will see which items on the front end menu. For example, I want some menu items which only show for logged in users and some which only show for users who have bought a subscription. The clients need to be able to look at the menu in the admin area and easily see which menu items are conditional, who will see them and which will show for everyone.
One way to do this is with conditional *menus*, ie make a menu for every possible combination of conditionals, so one for logged out users, one for logged in users who have not bought a subscription and one for logged in users who have bought a subscription. However this gets unmaintainable, especially because we already have different menus for various css breakpoints. Making a simple change to the order would require remembering to update multiple menus.
So I wanted conditional menu items – the individual items which make up a menu. Where to start? Putting the menu items in the sidebar so they could be chosen seemed like a good place. Luckily I learned it’s possible to make a custom menu metabox using add_meta_box like so:
I’m not going to tell you how to make a metabox here, it’s along the same lines as making other metaboxes. I, as usual, cheated by copying and editing someone else’s. In particular I started from WooCommerce’s menu metaboxes in add_nav_menu_meta_boxes().
But I will explain which fields are available and automatically saved in menu metaboxes. It helps to know that menu items are saved as individual posts with the post type of nav_menu_item. (The menus themselves are generated with all their associated menu items using terms but I’m not going into that here!)
Each menu item has (or can have) these default entries:
If those input field names are used in the menu item metabox, then wp_update_nav_menu_item() will automatically pick them up and save them where they should go. For the most part, I’ll get to the notable exception in a moment.
But first, notice what’s not in there. There’s no place to put the “item type label” – the label on the menu which says “Category” or “Custom Link”.
The function which renders those menu items is wp_setup_nav_menu_item().
wp_setup_nav_menu_item() sets up the menu item in the admin area and the front end. For the item type label in the admin, it looks for a few known menu item types, then defaults to set the label “Custom Link” (boo) but it sets the link as $menu_item->url (corresponding to menu-item-url) and the title as $menu_item->title (menu-item-title) so we’re golden, right?
It has a filter, so I can filter in the custom item type label by setting $item->type_label to be what I want. So far, so good. To figure out which label should be used, I’ll set a custom item type (menu-item-type in the metabox) and look for that, then use it to set the correct label. This will also make it easier on the front end to pick up which items should be conditionally rendered and when they should show. Sounds great, doesn’t it?
Let me try setting up the rest of my metabox so that menu-item-url links where I want, menu_item_title is the default title, maybe add some classes in menu-item-classes and see what happens.
Hmmm. This isn’t working as expected. The item type label is filtered in, no problem, but $menu_item->url has been unset so the front end menu item does not link to anything.
(Above is “the item type label is filtered in no problem” code.)
The issue with bumping up against older parts of WordPress core is that functions aren’t as flexible as one might expect. And that’s true here. The problem with wp_update_nav_menu_item() is that, if menu-item-type is *not* set as custom it strips the link in menu-item-url and tries to use menu-item-type to create a new url, leaving the created $menu_item->url blank if the menu item type is not in the hardcoded list. Then it saves the postmeta entry for the url as an empty string.
Luckily there’s an action at the end of the function where we update the postmeta row to the proper url. How to know what it should be though? Where can I put data that won’t get overwritten?
[Insert flailing about for a couple of hours. Really, I should have said to myself, “Self, stop working, it’s Friday afternoon and you’re done. Take a break and come back later.” Because when I came back to it, it was all much clearer.]
The short answer? Use menu-item-object-id for the id of the object and menu-item-object for the object type of the item to which the menu item links (ie not the menu item itself).
Why is the object type of the linked item necessary? That will tell me which function to use to get the link. It’ll be get_permalink for post objects, get_term_link for terms, etc. Then hook it all to 'wp_update_nav_menu_item' and update the postmeta which stores the url ('_menu_item_url') using $menu_item_db_id as the menu item’s post id. (Which is what it is.)
It works! Now all we have left to do is filter the menu items on the front end for our users.