Our seventh door in our Advent Calendar!

WordPress edit navigation menus – how, you wonder? I would like to present one of the packages we made. With it, you can add more menu fields in WordPress. In the following, I will give a short introduction.

The Problem: WordPress provides no Filter to edit the default Fields

Actually, WordPress provides a nice UI for editing navigation menus. However, it is quite opinionated about the available settings for each menu item. They are:

  • “Navigation Label”
  • “Title Attribute”
  • “Open link in a new tab”
  • “CSS Classes”
  • “Link Relationship (XFN)”
  • “Description”

So the problem is that WordPress provides no filter to edit the default fields. Moreover there’s no action hook to allow echoing custom form fields HTML, like happens in many other parts of WP backend.

Well, there’s one way to customize a filter hook. It’s the "wp_edit_nav_menu_walker" filter hook which allows to return the class name of a custom walker. So each time you need a field you need to write a custom walker. That’s quite annoying, but not a big deal. But things get worse when that filter is hooked by more than one plugin.

Because "wp_edit_nav_menu_walker" expects a walker class name it is only possible to completely override the class name. So if two or more plugins use that same filter, only one of them will get its walker class used. And the others will do nothing.

For our client projects we needed additional fields. These were for example “data” attributes or “rel” attributes (“noopener”, “nofollow”…). So we needed a solution for this problem. That’s why we created this package. It exists because we needed a way to add more fields that could work if used from more plugins.

The Solution: A Package Plugins can require via Composer

Because more than one plugin is going to use the use target of “More Menu Fields in WordPress”, it is not a plugin itself. Instead, it’s a package that plugins can require via Composer.


To install the package you need Composer, and PHP 7 or higher. The package name is inpsyde/more-menu-fields.


When the package is required via Composer and Composer autoload has been loaded, you need to bootstrap the package.

You can do it inside a plugin by just calling a function:


Inpsyde\MoreMenuFields\bootstrap();

There’s no need to wrap the call in any hook. If called more than once (by different plugins) nothing bad will happen.

The Field Interfaces

To add more fields it is necessary to create a PHP class for each of them. The class has to implement the interface Inpsyde\MoreMenuFields\EditField which looks like this:


interface EditField {

	public function name(): string;

	public function field_markup(): string;
}

The first method, name(), has to return the field name. It can be any string, but must be unique.

The second and last method, field_markup(), has to return the HTML markup for the field, as it will appear on the UI.

In the HTML markup it will very likely be necessary to use the input name, its id and its current stored value, if any. Those information can be obtained via an object of type Inpsyde\MoreMenuFields\EditFieldValue. More on this soon.

Very often (if not always) the value users enter in the generated input field needs to be sanitized before being saved. This is why the package ships another interface Inpsyde\MoreMenuFields\SanitizedEditField which looks like this:


interface SanitizedEditField extends EditField {

	public function sanitize_callback(): callable;
}

The interface extends EditField and you can use its only method, sanitize_callback(), to return a callback used to sanitize the users input. It is recommended to implement this interface to create fields and only use EditField for form input fields that don’t actually take input, like buttons.

You need an example? Check out GitHub where we added a field class example.

Adding a Field

Just having the field class above will do nothing if the package does not know about it. Therefore we need to make the package aware of the class. To do so we need to add an instance of it to the array passed by filter hook stored in the constant Inpsyde\MoreMenuFields\FILTER_FIELDS.

That filter passes to hooking callbacks the array of currently added filters, and as second argument an instance of EditFieldValueFactory: an object that can be used to obtain instances of EditFieldValue to be used in field classes.

Let’s see an usage example:


use Inpsyde\MoreMenuFields;
use My\Plugin\NofollowField;

add_filter(
    MoreMenuFields\FILTER_FIELDS,
    function (
        array $items,
        MoreMenuFields\EditFieldValueFactory $factory
    ) {
        $edit_field_value = $factory->create( 'nofollow' );
        $fields[] = new NofollowField( $edit_field_value );

        return $fields;
    },
    10,
    2
);

When hooking Inpsyde\MoreMenuFields\FILTER_FIELDS the passed EditFieldValueFactory is used to obtain an instance of EditFieldValue that is injected in the field object. (Nothing more than what I showed above).

To obtain the EditFieldValue instance the create() method is called on the factory, passing to it the name of the field, that must be the exact same name returned by field object name() method.

That’s it. The filter right above, plus the class in previous section is really all it takes to print the field and also save it.

The benefit of this can be seen when there are add many fields. Moreover, the Inpsyde\MoreMenuFields\FILTER_FIELDS filter can be used by many plugins that know nothing about each other and all will work just fine.

However, this shall only be a short introduction into our package. But actually, we have much more information. Do you want to know, for example, how to use the value stored by the added fields? Get all information on: https://github.com/inpsyde/more-menu-fields/blob/master/README.md

And tomorrow, there is a new great contribution in our Advent Calendar 2017!

Leave a reply

Your email address will not be published. Required fields are marked *