Using block editor SlotFills to render a meta box

Whether you were an early adopter of Gutenberg as a plugin or have used it as the block editor as of WordPress 5.0, you may have noticed that meta boxes added with the add_meta_box() function look and feel a little different than the rest of the panels in the Page sidebar.

A screenshot of the rendered UI when a meta box is registered via the add_meta_box PHP function.

Compared to the default “Discussion” panel above it, we can see that the custom meta box title is bold and its toggle icon is different. And if the postbox class is included in the markup of the custom meta box, we get a bit more UI to allow for reordering—which is useful if we have multiple PHP-registered meta boxes.

A screenshot of the rendered UI when a meta box is registered via the add_meta_box PHP function and includes the "postbox" class in its markup.

If these slight differences bug you, or you just want your feature to feel a little better integrated, you’re in luck!

Let’s look at a simple add_meta_box() example—a dropdown with a couple options—then implement it using a SlotFill to make it feel more like a default part of the block editor UI. This example is taken almost verbatim from the plugin handbook’s Custom Meta Boxes documentation, with translation, escaping, sanitization functions, and nonce checking added in.

The code is also reorganized to fit my personal preference—I like having the hook calls at the top of my files to serve as a kind of table of contents. And speaking of the hooks, I’ve used the post-type specific alternatives so the callbacks are triggered only for the post post type. But enough with the asides, here’s the example:

<?php
add_action( 'add_meta_boxes_post', 'my_add_custom_box' );
add_action( 'save_post_post', 'my_save_post_data' );

/**
 * Register the meta box.
 */
function my_add_custom_box() {
	add_meta_box(
		'my_box_id',
		esc_html__( 'Custom meta box title' ),
		'my_meta_box_html',
		'post',
		'side'
	);
}

/**
 * Display the metabox.
 *
 * @param WP_Post $post The post object.
 */
function my_meta_box_html( $post ) {
	wp_nonce_field( 'my_nonce_action', 'my_nonce_field' );
	$value = get_post_meta( $post->ID, '_my_meta_key', true );
	?>
	<label for="my_field"><?php esc_html_e( 'Field label' ); ?></label>
	<select name="my_field" id="my_field">
		<option value=""><?php esc_html_e( 'Select an option' ); ?></option>
		<option value="something" <?php selected( $value, 'something' ); ?>><?php esc_html_e( 'Something' ); ?></option>
		<option value="else" <?php selected( $value, 'else' ); ?>><?php esc_html_e( 'Else' ); ?></option>
	</select>
	<?php
}

/**
 * Save the metadata.
 *
 * @param int $post_id Post ID.
 */
function my_save_post_data( $post_id ) {
	if ( ! isset( $_POST['my_nonce_field'] ) || ! wp_verify_nonce( $_POST['my_nonce_field'], 'my_nonce_action' ) ) {
		return;
	}

	if ( array_key_exists( 'my_field', $_POST ) ) {
		update_post_meta( $post_id, '_my_meta_key', sanitize_text_field( $_POST['my_field'] ) );
	}
}Code language: PHP (php)

If you paste that into your theme’s functions.php file or dedicated include file or plugin, you should see a functional panel that looks like the first screenshot above in the Post sidebar when you create or edit a post.

Go ahead and remove this example so we don’t end up with conflicts as we build out a version using block editor APIs and components. The next example requires both PHP and JavaScript. As authored here, the JavaScript requires a build process, so let’s create this as a simple plugin.

I’m going to send you over to an overview on creating a custom plugin from a previous post to help get that set up. You can leave the PHP file blank aside from the header comment with the Plugin Name (which you can change to whatever you’d like), and you can stop once you’ve run npm install. (Changing the name and description in the package.json file is a good idea too, and you’ll want to activate the new plugin.)

After that’s all set, let’s add the following JavaScript to a file named my-meta-box.js within a src directory in our plugin folder:

import { SelectControl } from '@wordpress/components';
import { dispatch, withSelect } from '@wordpress/data';
import { PluginDocumentSettingPanel } from '@wordpress/edit-post';
import { __ } from '@wordpress/i18n';
import { registerPlugin } from '@wordpress/plugins';

const myMetaBox = withSelect( ( select ) => {
		const meta = select( 'core/editor' ).getEditedPostAttribute( 'meta' );

		return {
			metaValue: meta._my_meta_key,
		};
	} )( ( props ) => (
		<PluginDocumentSettingPanel
			title={ __( 'Custom meta box title' ) }
		>
			<SelectControl
				label={ __( 'Field label' ) }
				value={ props.metaValue }
				options={ [
					{ value: '', label: __( 'Select an option' ) },
					{ value: 'something', label: __( 'Something' ) },
					{ value: 'else', label: __( 'Else' ) },
				] }
				onChange={ ( value ) =>
					dispatch( 'core/editor' ).editPost( { meta: { _my_meta_key: value } } )
				}
			/>
		</PluginDocumentSettingPanel>
	)
);

registerPlugin( 'my-meta-box', { render: myMetaBox } );Code language: JavaScript (javascript)

Let’s break that down like MC Hammer before moving on. Most critically, we:

  • imported the registerPlugin method from wp.plugins and used it to — you guessed it! — register our meta box plugin for the block editor. You can think of this as a sort of analog to the add_meta_box() PHP function. The method has two parameters:
    • name, which must be unique per plugin, of course; and
    • settings, which accepts icon, render, and scope arguments. We only use render in our example, though it’s worth noting that icon defaults to plugins, so you could pass null for that if you don’t want any icon at all.
  • defined a method for rendering our metabox and passed its name as the render argument of the settings parameter to the registerPlugin method:
    • withSelect and dispatch are imported from wp.data so we can, respectively, pass data (like the meta value and current post type) to our underlying render function, and update the meta value when an option from the dropdown is selected;
    • select( 'core/editor' ).getEditedPostAttribute( 'meta' ) is used inside the withSelect higher-order component to retrieve all the post meta. Consider this functionally similar to using get_post_meta( $post_id ) in PHP. We grab the value from the key we need using dot notation, then pass it to our underlying function.
    • The editPost method of dispatch( 'core/editor' ) is used similarly to how setAttributes() is used in a block. We’re dealing with post meta here, though, so the object we  pass is another object (our custom key paired with the onChange value) on the meta key.
    • PluginDocumentSettingPanel is imported from wp.editPost — this is the SlotFill that renders a panel in the document sidebar, so we wrap our JSX markup in this component.

The rest should be familiar if you’ve worked with blocks, and is hopefully simple enough to be intuitive even if you haven’t, so let’s move on to the required PHP. We need to register our meta key and make sure our JavaScript loads up in the block editor so we can actually see our custom panel! Within the plugin.php file, add:

add_action( 'init', 'my_register_meta' );
add_action( 'enqueue_block_editor_assets', 'my_enqueue_block_editor_assets' );

/**
 * Register meta key.
 */
function my_register_meta() {
	register_post_meta(
		'post',
		'_my_meta_key',
		array(
			'auth_callback'     => function() {
				return current_user_can( 'edit_posts' );
			},
			'default'           => '',
			'enum'              => array( 'something', 'else' ),
			'sanitize_callback' => 'sanitize_text_field',
			'show_in_rest'      => true,
			'single'            => true,
			'type'              => 'string',
		)
	);
}

/**
 * Enqueue assets for the block editor.
 */
function my_enqueue_block_editor_assets() {
	if ( 'post' === get_current_screen()->id ) {
		$asset_data = require_once __DIR__ . '/build/my-meta-box.asset.php';

		wp_enqueue_script(
			'my-meta-box',
			plugins_url( '/slot-fill/build/my-meta-box.js', __DIR__ ),
			$asset_data['dependencies'],
			$asset_data['version'],
			true
		);
	}
}Code language: PHP (php)

One gotcha: make sure to put the correct plugin folder in line 39. If you used the Primary Category Permalinks plugin example verbatim, that will be /primary-category-permalinks/build/my-meta-box.js instead of /slot-fill/build/my-meta-box.js.

Hammer time! The register_post_meta() function is just a wrapper for the register_meta() function. It handles setting the object_subtype argument to the default post type—not the biggest lift, but if we have a shortcut available, we may as well use it. We pass three parameters to it: the post type, our meta key, and an array of arguments.

To learn about all the available arguments, see WordPress.org’s register_meta code reference. Most importantly, we need to set show_in_rest to true in order for the dispatch( 'core/editor' ).editPost call in our JavaScript to work. Making sure that the type argument matches with the kind of data we’re storing on our meta key is critical as well.

Using the enqueue_block_editor_assets hook and the wp_enqueue_script function, we load our JavaScript for the block editor. When we run npm run build in the terminal—which, if you haven’t done so after adding the JavaScript file, here’s a friendly reminder to run it now—an asset file is conveniently generated for us. We leverage this file for the dependencies and version parameters in our call to the wp_enqueue_script function.

Finally, we’ve wrapped that call in a check against the id returned by the get_current_screen function so our script only loads on the edit screen for posts.

Now our “metabox” looks like this:

A screenshot of the rendered UI when the "PluginDocumentSettingPanel" is used.

But wait—now it’s right under the “Status & visibility” panel instead of at the bottom of the sidebar, and we’ve lost the ability to reorder it. These are a couple of the current trade offs to using this approach. Although it looks more like a built-in part of the interface now, we have no control over where it appears in the sidebar like we used to with the priority parameter of the add_meta_box PHP function. Well, that’s not entirely true—the render order is currently based on the registration order, so there is some limited organizing capability there.

And we deliberately used the document sidebar SlotFill in this example, but if your old metabox uses the normal or advanced context because it needs the extra real estate afforded by the content column, you’ll need to consider other options if you make the switch to this approach. Of the available options—check out currently available slotfills and examples for the full list—PluginSidebar might work for your case. If that still doesn’t work out, using a SlotFill may not be the answer, but a combination of a block with meta attributes and a block pattern could be.

The block editor offers many ways for us to extend it to meet our needs and the needs of our clients. Perhaps too many in some cases, which can understandably be overwhelming and frustrating at times. And by no means is it perfect, or even appropriate for every use case—nothing ever is. But I encourage you to experiment with it and keep yourself informed of its development even so.

I also encourage you to get involved if you have ideas for how it can be improved, even if just by opening an issue on Github. The Gutenberg team has done and continues to do excellent work to provide a more modern editing experience, and I, for one, am excited to see how it evolves!

Leave a Reply