Monthly Archives: April 2017

How to Add Custom Fields to WordPress Navigation Menu Items

The task of adding custom fields to WordPress navigation menus has come up a few times in my work, so I figured I should document the process which I have learned since it is fairly complex and not very well documented on the internet to this date. Kudos to those of you who have figured this out and documented it because it is one of the more advanced tasks involving WordPress that I have had to do. I am not sure why WordPress made it so complex to alter the navigation menu infrastructure, but to be a true WordPress expert, you do need to learn to work with it, so I aim to help others learn, as I have, how to manipulate the WordPress navigation menus with as much ease as possible.

This is by far one of the more advanced tutorials for WordPress, so if you don't feel like going through all of the steps below. I've already made a plugin for this that will be for sale on my site. If you want to get an advance copy of it email me at linian11@yahoo.com and I can help you out.

Adding custom Fields to WordPress Navigation Menu Items

To be perfectly clear, here is a screenshot of the default menu editing screen after I made my initial top navigation menu for my site, Jafty.com with no added custom fields:

nav1

In contrast, here is the same screen under appearance/menus after I have added two custom fields to it using the methods I'll describe below:

nav2

Notice in the above image, the addition of the "Icon URL" and "Custom Field #2" fields. These I added with my custom plugin. Read on to find out how to add similar fields and how to access them or display them on the front-end of your site.

On the front-end, I added two icons to the menu real quick so you can see what it looks like:

iconnav

The above image is a sub nav that pops up when you click on "Projects" and I added icons to the first two sub-links using the new "icon URL" field added with the method I'm about to show you...

THE CODE:

So to accomplish this, I started a new plugin called "Jafty Top Nav". I made a folder named jafty-top-nav and a file inside it named jafty-top-nav.php with the following contents:

<?php
/**
* Plugin Name: Jafty Top Nav
* Plugin URI: http://jafty.com/
* Description: A Plugin that adds a custom top navigation menu to WordPress. This also adds the icon menu option to the WordPress admin, allowing one to select an icon to appear before submenu items in submenu dropdowns.
* Version: 1.0
* Author: Ian L. of Jafty.com
* Author URI: http://jafty.com
* License: GPL2
*/

//adds the Primary Jafty Menu location to wp-admin:
add_action('after_setup_theme', 'register_jafty_menu');
function register_jafty_menu(){
register_nav_menu('jafty-top-nav', __('Primary Jafty Menu', 'jafty-top-nav'));
}

//includes the library used to add custom fields to menu items:
require_once dirname( __FILE__ ) . '/menu-item-custom-fields/menu-item-custom-fields.php';

//include 'http://jafty.com/wp-content/plugins/jafty-top-nav-plugin/menu-item-custom-fields.php';
include 'menu-item-custom-fields.php';

class Jafty_Walker extends Walker_Nav_Menu {

// Displays start of an element. E.g '<li> Item Name'
// @see Walker::start_el()
function start_el(&$output, $item, $depth=0, $args=array(), $id = 0) {
$object = $item->object;
$type = $item->type;
$title = $item->title;
$description = $item->description;
$permalink = $item->url;
$icon = get_post_meta($item->ID, 'menu-item-icon-url', true);
$output .= "<li class='" .  implode(" ", $item->classes) . "'>";

//Add SPAN if no Permalink
if( $permalink && $permalink != '#' ) {
$output .= '<img src="'.$icon.'" style="width:25px;float:left;margin-right:10px;margin-top:3px" /><a href="' . $permalink . '">';
} else {
$output .= '<span>';
}

$output .= $title;
if( $description != '' && $depth == 0 ) {
$output .= '<small class="description">' . $description . '</small>';
}
if( $permalink && $permalink != '#' ) {
$output .= '</a>';
} else {
$output .= '</span>';
}
}
}

?>

The most important portion of code above I put in red text. That is where I retrieve one of the custom fields in order to display it in the nav menu. You will notice below that I named the field "icon-url" in the "jafty-custom-nav-fields.php" file and above I reference it with "menu-item-icon-url", so take notice of this so you don't cause an error.

Then I created a second file inside the same "jafty-top-nav-plugin" folder named "jafty-custom-nav-fields.php" which has the following code:

<?php
/**
* Custom menu items metadata:
*/
class Jafty_Custom_Nav_Fields {

/**
* Holds our custom fields
*/
protected static $fields = array();

/**
* Initialize plugin
*/
public static function init() {
add_action( 'wp_nav_menu_item_custom_fields', array( __CLASS__, '_fields' ), 10, 4 );
add_action( 'wp_update_nav_menu_item', array( __CLASS__, '_save' ), 10, 3 );
add_filter( 'manage_nav-menus_columns', array( __CLASS__, '_columns' ), 99 );

self::$fields = array(
//note that menu-item- gets prepended to field names
//i.e.: field-01 becomes menu-item-field-01
//i.e.: icon-url becomes menu-item-icon-url
'icon-url' => __( 'Icon URL:', 'jafty_custom_nav_fields' ),
'field-02' => __( 'Custom Field #2', 'jafty_custom_nav_fields' ),
);
}

/**
* Save custom field value
*
* @wp_hook action wp_update_nav_menu_item
*
* @param int   $menu_id         Nav menu ID
* @param int   $menu_item_db_id Menu item ID
* @param array $menu_item_args  Menu item data
*/
public static function _save( $menu_id, $menu_item_db_id, $menu_item_args ) {
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
return;
}

check_admin_referer( 'update-nav_menu', 'update-nav-menu-nonce' );

foreach ( self::$fields as $_key => $label ) {
$key = sprintf( 'menu-item-%s', $_key );

// Sanitize
if ( ! empty( $_POST[ $key ][ $menu_item_db_id ] ) ) {
// Do some checks here...
$value = $_POST[ $key ][ $menu_item_db_id ];
} else {
$value = null;
}

// Update
if ( ! is_null( $value ) ) {
update_post_meta( $menu_item_db_id, $key, $value );
echo "key:$key<br />";
} else {
delete_post_meta( $menu_item_db_id, $key );
}
}
}

/**
* Print field
*
* @param object $item  Menu item data object.
* @param int    $depth  Depth of menu item. Used for padding.
* @param array  $args  Menu item args.
* @param int    $id    Nav menu ID.
*
* @return string Form fields
*/
public static function _fields( $id, $item, $depth, $args ) {
foreach ( self::$fields as $_key => $label ) :
$key   = sprintf( 'menu-item-%s', $_key );
$id    = sprintf( 'edit-%s-%s', $key, $item->ID );
$name  = sprintf( '%s[%s]', $key, $item->ID );
$value = get_post_meta( $item->ID, $key, true );
$class = sprintf( 'field-%s', $_key );
?>
<p class="description description-wide <?php echo esc_attr( $class ) ?>">
<?php printf(
'<label for="%1$s">%2$s<br /><input type="text" id="%1$s" class="widefat %1$s" name="%3$s" value="%4$s" /></label>',
esc_attr( $id ),
esc_html( $label ),
esc_attr( $name ),
esc_attr( $value )
) ?>
</p>
<?php
endforeach;
}

/**
* Add our fields to the screen options toggle
*
* @param array $columns Menu item columns
* @return array
*/
public static function _columns( $columns ) {
$columns = array_merge( $columns, self::$fields );

return $columns;
}
}
Jafty_Custom_Nav_Fields::init();
?>

The most important part of the above code is the field names. I changed the text color for the field names to red. However you also need to be aware that you have to append "menu-icon-url" to the field names when you go to fetch them in your header.php theme file later on so icon-url would become icon-url-menu-icon-url" for example...see the note above the code as well.

Then I used two files created by  Dzikri Aziz and put them in their own folder named "menu-item-custom-fields" inside of the jafty-top-nav folder. Here are the names and content of those two files if you are copy and pasting as we go:

menu-item-custom-fields.php content:

<?php

/**
 * Menu Item Custom Fields
 */

if ( ! class_exists( 'Menu_Item_Custom_Fields' ) ) :
    /**
    * Menu Item Custom Fields Loader
    */
    class Menu_Item_Custom_Fields {

        /**
        * Add filter
        *
        * @wp_hook action wp_loaded
        */
        public static function load() {
            add_filter( 'wp_edit_nav_menu_walker', array( __CLASS__, '_filter_walker' ), 99 );
        }

        /**
        * Replace default menu editor walker with ours
        *
        * We don't actually replace the default walker. We're still using it and
        * only injecting some HTMLs.
        *
        * @since   0.1.0
        * @access  private
        * @wp_hook filter wp_edit_nav_menu_walker
        * @param   string $walker Walker class name
        * @return  string Walker class name
        */
        public static function _filter_walker( $walker ) {
            $walker = 'Menu_Item_Custom_Fields_Walker';
            if ( ! class_exists( $walker ) ) {
                require_once dirname( __FILE__ ) . '/walker-nav-menu-edit.php';
            }

            return $walker;
        }
    }
    add_action( 'wp_loaded', array( 'Menu_Item_Custom_Fields', 'load' ), 9 );
endif; // class_exists( 'Menu_Item_Custom_Fields' )

// Uncomment the following line to test this plugin
#require_once dirname( __FILE__ ) . '/doc/menu-item-custom-fields-example.php';

walker-nav-menu-edit.php content:

<?php

/**
 * Custom Walker for Nav Menu Editor
 *
 */
class Menu_Item_Custom_Fields_Walker extends Walker_Nav_Menu_Edit {

    /**
     * Start the element output.
     *
     * We're injecting our custom fields after the div.submitbox
     *
     * @see Walker_Nav_Menu::start_el()
     * @since 0.1.0
     * @since 0.2.0 Update regex pattern to support WordPress 4.7's markup.
     *
     * @param string $output Passed by reference. Used to append additional content.
     * @param object $item   Menu item data object.
     * @param int    $depth  Depth of menu item. Used for padding.
     * @param array  $args   Menu item args.
     * @param int    $id     Nav menu ID.
     */
    function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
        $item_output = '';

        parent::start_el( $item_output, $item, $depth, $args, $id );

        $output .= preg_replace(
            // NOTE: Check this regex from time to time!
            '/(?=<(fieldset|p)[^>]+class="[^"]*field-move)/',
            $this->get_fields( $item, $depth, $args ),
            $item_output
        );
    }

    /**
     * Get custom fields
     *
     * @access protected
     * @since 0.1.0
     * @uses add_action() Calls 'menu_item_custom_fields' hook
     *
     * @param object $item   Menu item data object.
     * @param int    $depth  Depth of menu item. Used for padding.
     * @param array  $args   Menu item args.
     * @param int    $id     Nav menu ID.
     *
     * @return string Form fields
     */
    protected function get_fields( $item, $depth, $args = array(), $id = 0 ) {
        ob_start();

        /**
         * Get menu item custom fields from plugins/themes
         *
         * @since 0.1.0
         * @since 1.0.0 Pass correct parameters.
         *
         * @param int    $item_id  Menu item ID.
         * @param object $item     Menu item data object.
         * @param int    $depth    Depth of menu item. Used for padding.
         * @param array  $args     Menu item args.
         * @param int    $id       Nav menu ID.
         *
         * @return string Custom fields HTML.
         */
        do_action( 'wp_nav_menu_item_custom_fields', $item->ID, $item, $depth, $args, $id );

        return ob_get_clean();
    }
}

Then finally, the last thing you have to do is edit your header.php file for your current WordPress theme to include something like this where your to site navigation code appears:

<nav id="site-navigation" class="main-navigation" role="navigation">
            <button class="menu-toggle" aria-controls="primary-menu" aria-expanded="false"><?php esc_html_e( 'Primary Jafty Menu', 'jafty-theme' ); ?></button>
            <?php wp_nav_menu( array( 'theme_location' => 'menu-1', 'menu_id' => 'primary-jafty-menu', 'walker' => new Jafty_Walker() ) ); ?>
        </nav><!-- #site-navigation -->

Summary

That's all there is to it. I know it's a lot, but if you start out by copy and pasting my code and following instructions carefully to recreate the above jafty-top-nav plugin, it should work fine. Afterwards, you can edit it to fit your own personal requirements as needed. Good luck!

 

I want to give credit for the help I got on this technique, but frankly I saw the same code in different areas online and don't know which is the correct site to link to, but in one of the files, the author gave his name, which was Dzikri Aziz, so I'd like to extend my thanks to Dzikri Aziz for helping me bring this code to life. If I find a link I'll post it or if anyone knows it, please comment and I'll make sure he gets the appropriate credit for his portion of the code I used in this article.