Custom WordPress and WooCommerce Development

Add Custom Fields to WordPress Menu Items

First a little backstory

A WordPress commit that I am personally very excited to finally see get merged into core is the addition of a wp_nav_menu_item_custom_fields hook!

As the author of the Nav Menu Roles plugin, I’ve needed to display custom fields on menu items. But in the admin, the menu items are displayed by the Walker_Nav_Menu_Edit class and there have never been hooks in there to customize the output. The only 2 options for customizing the output were inserting fields via JavaScript or entirely replacing the Walker with your own customized Walker via the wp_edit_nav_menu_walker filter.

A WordPress Menu item metabox displaying options for hiding or showing this menu item to users based on their role.

The latter is what I did in my plugin, but the problem with the walker filter is that There Can Be Only One!

There Can Be Only One Highlander GIF - Find & Share on GIPHY

So other plugins (and themes…. cough…. soooo many themes!) that also needed to add custom fields would add their own Walkers and we’d duel it out to see whose fields got displayed.

Then WordPress user Shazdeh had the brilliant idea to start inserting a standard hook in our own Walkers… figuring that if we all added that hook to our Walker and then all attached our fields to that hook, it would not matter whose Walker was being used by core all our plugins could be compatible. We essentially established a “community” action hook and it greatly reduced the number of people contacting me wondering why Nav Menu Role’s fields were not displaying.

I expected that to be the permanent solution as the original Trac ticket was opened by @ViperBond back in 2011.

Originally it was blocked by another ticket concerning the limit of _POST variables… folks with ginormous menus would see their menus disappear when the _POST limit was exceeded and core didn’t want to make it easier for us to add fields. But that was fixed and this hook still languished. It would see a little traction and then nothing.

I actually went to my first contributor day to work on this specifically (it’s a 4 line patch, I thought it would be easy enough). I ended up spending the entire day trying to get VVV setup on my Windows machine so I could have the exact same environment as everyone else. Spoiler: I never did. I forget his name, but there was a gent who was trying so hard to help me and I thanked him, but it was so discouraging to go to a Contributor day and do nothing.

I’d pretty much given up hope on this one, but @MikeSchinkel picked it up and kept advocating for it. And now finally @SergeyBiryukov committed it! Even adding parity with the Customizer (which was another reason the patch had been rejected before). Hurray!!

Big thank you to everyone who has had a part in this over the years!

Leonardo Dicaprio Rap GIF - Find & Share on GIPHY

The Code Parts

Now that wp_nav_menu_item_custom_fields is in WordPress core, here’s a tutorial on how to use that new hook.

First, we’ll attach a callback to the new hook and use it to display a text input. Because the Walker displays multiple menu items we have to make sure the input is keyed with the menu item’s ID.

/**
* Add custom fields to menu item
*
* This will allow us to play nicely with any other plugin that is adding the same hook
*
* @param  int $item_id 
* @params obj $item - the menu item
* @params array $args
*/
function kia_custom_fields( $item_id, $item ) {

	wp_nonce_field( 'custom_menu_meta_nonce', '_custom_menu_meta_nonce_name' );
	$custom_menu_meta = get_post_meta( $item_id, '_custom_menu_meta', true );
	?>
	<div class="field-custom_menu_meta description-wide" style="margin: 5px 0;">
	    <span class="description"><?php _e( "Extra Field", 'custom-menu-meta' ); ?></span>
	    <br />

	    <input type="hidden" class="nav-menu-id" value="<?php echo $item_id ;?>" />

	    <div class="logged-input-holder">
	        <input type="text" name="custom_menu_meta[<?php echo $item_id ;?>]" id="custom-menu-meta-for-<?php echo $item_id ;?>" value="<?php echo esc_attr( $custom_menu_meta ); ?>" />
	        <label for="custom-menu-meta-for-<?php echo $item_id ;?>">
	            <?php _e( 'Custom menu text', 'custom-menu-meta'); ?>
	        </label>
	    </div>

	</div>

	<?php
}
add_action( 'wp_nav_menu_item_custom_fields', 'kia_custom_fields', 10, 2 );Code language: JavaScript (javascript)

Since menu items are a WordPress custom post type, dealing with the data is same as adding and retrieving post_meta a regular post or page.

/**
* Save the menu item meta
* 
* @param int $menu_id
* @param int $menu_item_db_id	
*/
function kia_nav_update( $menu_id, $menu_item_db_id ) {

	// Verify this came from our screen and with proper authorization.
	if ( ! isset( $_POST['_custom_menu_meta_nonce_name'] ) || ! wp_verify_nonce( $_POST['_custom_menu_meta_nonce_name'], 'custom_menu_meta_nonce' ) ) {
		return $menu_id;
	}

	if ( isset( $_POST['custom_menu_meta'][$menu_item_db_id]  ) ) {
		$sanitized_data = sanitize_text_field( $_POST['custom_menu_meta'][$menu_item_db_id] );
		update_post_meta( $menu_item_db_id, '_custom_menu_meta', $sanitized_data );
	} else {
		delete_post_meta( $menu_item_db_id, '_custom_menu_meta' );
	}
}
add_action( 'wp_update_nav_menu_item', 'kia_nav_update', 10, 2 );Code language: PHP (php)

Finally, what do we do with this new data!? While this isn’t what my plugin does, it’s the simplest use-case I could think of… displaying some extra text after the menu title.

/**
* Displays text on the front-end.
*
* @param string   $title The menu item's title.
* @param WP_Post  $item  The current menu item.
* @return string      
*/
function kia_custom_menu_title( $title, $item ) {

	if( is_object( $item ) && isset( $item->ID ) ) {

		$custom_menu_meta = get_post_meta( $item->ID, '_custom_menu_meta', true );

		if ( ! empty( $custom_menu_meta ) ) {
			$title .= ' - ' . $custom_menu_meta;
		}
	}
	return $title;
}
add_filter( 'nav_menu_item_title', 'kia_custom_menu_title', 10, 2 );Code language: PHP (php)

The three snippets together make a pretty solid minimal example for how to use this new hook. Full code can be downloaded here. At some point I should come back and do something with the new hooks in the Customizer too as I guess I finally need to update Nav Menu Roles!


Comments

13 responses to “Add Custom Fields to WordPress Menu Items”

  1. Thank you very much for this tutorial.
    It works fine.

    However, I stumbled across an issue when I add a select box.
    When I add a menu item to the menu, the select is not displayed as selectbox, but instead in plain text?
    Do you have a solution for this? Am I missing something?

    See here:
    http://spab-rice.com/select.png

    Another issue is that the values of custom fields are not included to import/export.

    1. Unfortunately, I don’t know why your select input isn’t displaying properly. Are you perhaps escaping things too aggressively? Could you use a number input there for the number of columns?

      And you are correct that menu meta isn’t included in the import/export. I was wondering if there was a ticket for that, but it looks like I created that ticket 7 years ago.

      https://plugins.trac.wordpress.org/ticket/1678

      In the meantime, I’ve built my own meta importer into Nav Menu Roles and you can take a look at it for some inspiration:
      https://github.com/helgatheviking/Nav-Menu-Roles/blob/master/inc/class.Nav_Menu_Roles_Import.php

  2. You use wp_nonce_field() function so you don’t need to include any hidden field for a nonce.

    1. Good catch

  3. Thi is a great tutorial and kudos for the addition to core. I am adding code based on this to one of my repisutory plugins.

    What is the aprent theme you are using here btw

    1. Glad you liked it Peter. The theme is currently the Beaver Builder Theme.

  4. Nicholas Rees Avatar
    Nicholas Rees

    Kathy,

    Do you know if it would be possible to add something like WP_Customize_Color_Control ( https://developer.wordpress.org/reference/classes/wp_customize_color_control/ ). This could potentially allow users to customize the menu text/background on a per item basis in the customizer (Basically what I’m trying to do). However, I’m a rather new to word press development and I’m not entirely sure how to go about doing this.

    Right now my plan is to go through the code that makes that class work and just use it as my own, but I was wondering if you’d have an idea that I could use more built in features in the core.

    Any help with this would really be appreciated!

    1. Hi Nicolas. I’m working on Nav Menu Roles 2.0 which will add my role selection checkboxes/radios to the Customizer. I had to get a lot of help because my Javascript is pretty toast and as you’ve probably discovered this isn’t documented yet at all. So Weston Ruter (who wrote a lot of the Customizer) graciously assisted me and you can see his full reply to my question at wordpress.stackexchange.com. And you can see how I’ve integrated it into my plugin in these two files: customizer.php and customizer.js.

      I hope that helps!

      1. Nicholas Rees Avatar
        Nicholas Rees

        Yes, it does help, because now I’m not trying to tear my hair out trying to do something that is either beyond my capabilities or impossible based off of what I have to work with.

        I’m pretty new at this, but I’ll update you if I can figure out an “interesting” solution (and I have one or two ideas). I really don’t like messing with java script either, but in this case it looks like I have no choice.

        1. Working in the Customizer, yes.. there’s no choice but to use Javascript. I guess if we want to stay in WP we need to get dragged along to JS eventually. :) Do let me know if you figure it out!

  5. Is there a way to show the custom fields on “add to menu”? It works, but the custom fields only show after saving.

    1. Do you mean in the admin? I can’t reproduce that. See: https://share.getcloudapp.com/4guJdEAl I’d check your theme and other plugins for conflicts.

  6. I had to generate the nonce specific to the menu item ID (similar reasons you do it for the field input) to avoid DOM complaints about non-unique hidden input elements related to the nonce. Accordingly I appended the menu item’s database id (i.e. the menu item id) to the line generating nonce and in the code confirming authenticity/saving the value. With the change things work as intended without the console DOM warnings.