If you want to add a new sidebar control to an existing block, there are filters for that. Or if it’s your own block, you can just use InspectorControls for that. But if you wand to add global settings, which reflect something for the whole post, you need to do it differently.

Since I couldn’t find a proper solution already described in a blog post or documentation, here’s my own one.

What we basically need it registering a plugin and use the PluginDocumentSettingPanel to add a new panel to the main document settings. Additionally, to store the value(s) of the panel, you need to save it as post meta in the database. My example is a part of a Facebook plugin that lets you choose which conversion types are triggered by visiting the page. Also, this is just the React part. Especially to get saving the meta value working, make sure to properly register the field so that it is available via REST API.

To register the plugin, we use registerPlugin from @wordpress/plugins:

registerPlugin( 'my-facebook-conversions-api-meta-box', {
	icon: '',
	render: () => {
		return ( <PluginDocumentSettingPanel
			name="my-facebook-conversions-api-panel"
			title={ __( 'Facebook Conversions', 'my-facebook-conversions-api' ) }
			className="my-facebook-conversions-api-panel"
		>
			<div>My Panel Content</div>
		</PluginDocumentSettingPanel> );
	},
} );
Code language: JavaScript (javascript)

This will add a new panel with a div inside. I’ve left the icon defined, but empty, as there is a default icon displayed alongside the panel’s title, which is, in my opinion, a little misplaced and also the default panels have no such icon. The name is an identifier for the panel and should be unique. The title is what’s getting displayed as panel title and className allows you to add custom classes to the panel.

In my case, I wanted to restrict the panel to only show up on pages, so I added a check for the post type as well:

import { useSelect } from '@wordpress/data';
import { PluginDocumentSettingPanel } from '@wordpress/edit-post';
import { __ } from '@wordpress/i18n';
import { registerPlugin } from '@wordpress/plugins';

registerPlugin( 'my-facebook-conversions-api-meta-box', {
	icon: '',
	render: () => {
		const postType = useSelect(
			( select ) => select( 'core/editor' ).getCurrentPostType(),
			[]
		);
		
		// ignore post types other than page
		if ( postType !== 'page' ) {
			return null;
		}
		
		return ( <PluginDocumentSettingPanel
			name="my-facebook-conversions-api-panel"
			title={ __( 'Facebook Conversions', 'my-facebook-conversions-api' ) }
			className="my-facebook-conversions-api-panel"
		>
			<div>My Panel Content</div>
		</PluginDocumentSettingPanel> );
	},
} );
Code language: JavaScript (javascript)

The most important part, displaying the panel, should now be done. But we need to make sure to store the data properly. In my case, I use a custom control that is a list of checkboxes, where multiple checkboxes can be checked, but their data is stored together in a single post meta field. The control looks like this:

import { CheckboxControl } from '@wordpress/components';
import { useEntityProp } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';

const ConversionTypeControl = ( () => {
	const postType = useSelect(
		( select ) => select( 'core/editor' ).getCurrentPostType(),
		[]
	);
	const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' );
	const metaFieldValue = meta.my_facebook_conversions_api_conversion_type;
	const updateMetaValue = ( option, value ) => {
		// make sure the value gets updated correctly
		// @see https://stackoverflow.com/questions/56452438/update-a-specific-property-of-an-object-attribute-in-a-wordpress-gutenberg-block#comment99517264_56459084
		let newValue = JSON.parse( JSON.stringify( meta ) );
		
		if ( ! newValue.my_facebook_conversions_api_conversion_type ) {
			newValue = {
				my_facebook_conversions_api_conversion_type: [],
			};
		}
		
		if ( ! newValue.my_facebook_conversions_api_conversion_type.includes( option ) && value ) {
			newValue.my_facebook_conversions_api_conversion_type.push( option );
		}
		else if ( ! value ) {
			const index = newValue.my_facebook_conversions_api_conversion_type.indexOf( option );
			
			if ( index > -1 ) {
				newValue.my_facebook_conversions_api_conversion_type.splice( index, 1 );
			}
		}
		
		setMeta( { ...meta, ...newValue } );
	};
	
	const options = [
		{ label: __( 'Add Payment Info', 'my-facebook-conversions-api' ), value: 'AddPaymentInfo' },
		{ label: __( 'Add to Cart', 'my-facebook-conversions-api' ), value: 'AddToCart' },
		{ label: __( 'Add to Wishlist', 'my-facebook-conversions-api' ), value: 'AddToWishlist' },
		{ label: __( 'Complete Registration', 'my-facebook-conversions-api' ), value: 'CompleteRegistration' },
		{ label: __( 'Contact', 'my-facebook-conversions-api' ), value: 'Contact' },
		{ label: __( 'Customize Product', 'my-facebook-conversions-api' ), value: 'CustomizeProduct' },
		{ label: __( 'Donate', 'my-facebook-conversions-api' ), value: 'Donate' },
		{ label: __( 'Find Location', 'my-facebook-conversions-api' ), value: 'FindLocation' },
		{ label: __( 'Initiate Checkout', 'my-facebook-conversions-api' ), value: 'InitiateCheckout' },
		{ label: __( 'Lead', 'my-facebook-conversions-api' ), value: 'Lead' },
		{ label: __( 'Page View', 'my-facebook-conversions-api' ), value: 'PageView' },
		{ label: __( 'Schedule Appointment', 'my-facebook-conversions-api' ), value: 'Schedule' },
		{ label: __( 'Search', 'my-facebook-conversions-api' ), value: 'Search' },
		{ label: __( 'Submit Application', 'my-facebook-conversions-api' ), value: 'SubmitApplication' },
		{ label: __( 'View Content', 'my-facebook-conversions-api' ), value: 'ViewContent' },
	];
	
	return ( <div className="my-facebook-conversions-api__meta-box my-facebook-conversions-api__meta-box--conversion-type">
		<p>{ __( 'Conversion type:', 'my-facebook-conversions-api' ) }</p>
		{ Object.values( options ).map( ( option, index ) => {
			return ( <CheckboxControl
				key={ option + index }
				label={ option[ 'label' ] }	
				checked={ metaFieldValue?.includes( option['value'] ) }
				value={ option[ 'value' ] }
				onChange={ ( value ) => updateMetaValue( option[ 'value' ], value ) }
			/> )
		} ) }
	</div> );
} );
Code language: JavaScript (javascript)

From top to bottom: To store the data for the correct post type, we need to get it first (line 7 – 10). To be able to store it then, we use useEntityProp, which allows us to easily get and set given metadata (line 11). So we get the current metadata in line 12 first. The constant updateMetaValue refers to a custom function to update the metadata properly so that multiple values can be stored (hint: in PHP, I registered the meta field as type array).

The options in line 38 – 54 are iterated in line 58 – 66 and return a checkbox for each option, which is returned by the control itself.

This control is then used in the render function of registerPlugin (line 90). So the full code would be:

import { CheckboxControl } from '@wordpress/components';
import { useEntityProp } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { PluginDocumentSettingPanel } from '@wordpress/edit-post';
import { __ } from '@wordpress/i18n';
import { registerPlugin } from '@wordpress/plugins';

const ConversionTypeControl = ( () => {
	const postType = useSelect(
		( select ) => select( 'core/editor' ).getCurrentPostType(),
		[]
	);
	const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' );
	const metaFieldValue = meta.my_facebook_conversions_api_conversion_type;
	const updateMetaValue = ( option, value ) => {
		// make sure the value gets updated correctly
		// @see https://stackoverflow.com/questions/56452438/update-a-specific-property-of-an-object-attribute-in-a-wordpress-gutenberg-block#comment99517264_56459084
		let newValue = JSON.parse( JSON.stringify( meta ) );
		
		if ( ! newValue.my_facebook_conversions_api_conversion_type ) {
			newValue = {
				my_facebook_conversions_api_conversion_type: [],
			};
		}
		
		if ( ! newValue.my_facebook_conversions_api_conversion_type.includes( option ) && value ) {
			newValue.my_facebook_conversions_api_conversion_type.push( option );
		}
		else if ( ! value ) {
			const index = newValue.my_facebook_conversions_api_conversion_type.indexOf( option );
			
			if ( index > -1 ) {
				newValue.my_facebook_conversions_api_conversion_type.splice( index, 1 );
			}
		}
		
		setMeta( { ...meta, ...newValue } );
	};
	
	const options = [
		{ label: __( 'Add Payment Info', 'my-facebook-conversions-api' ), value: 'AddPaymentInfo' },
		{ label: __( 'Add to Cart', 'my-facebook-conversions-api' ), value: 'AddToCart' },
		{ label: __( 'Add to Wishlist', 'my-facebook-conversions-api' ), value: 'AddToWishlist' },
		{ label: __( 'Complete Registration', 'my-facebook-conversions-api' ), value: 'CompleteRegistration' },
		{ label: __( 'Contact', 'my-facebook-conversions-api' ), value: 'Contact' },
		{ label: __( 'Customize Product', 'my-facebook-conversions-api' ), value: 'CustomizeProduct' },
		{ label: __( 'Donate', 'my-facebook-conversions-api' ), value: 'Donate' },
		{ label: __( 'Find Location', 'my-facebook-conversions-api' ), value: 'FindLocation' },
		{ label: __( 'Initiate Checkout', 'my-facebook-conversions-api' ), value: 'InitiateCheckout' },
		{ label: __( 'Lead', 'my-facebook-conversions-api' ), value: 'Lead' },
		{ label: __( 'Page View', 'my-facebook-conversions-api' ), value: 'PageView' },
		{ label: __( 'Schedule Appointment', 'my-facebook-conversions-api' ), value: 'Schedule' },
		{ label: __( 'Search', 'my-facebook-conversions-api' ), value: 'Search' },
		{ label: __( 'Submit Application', 'my-facebook-conversions-api' ), value: 'SubmitApplication' },
		{ label: __( 'View Content', 'my-facebook-conversions-api' ), value: 'ViewContent' },
	];
	
	return ( <div className="my-facebook-conversions-api__meta-box my-facebook-conversions-api__meta-box--conversion-type">
		<p>{ __( 'Conversion type:', 'my-facebook-conversions-api' ) }</p>
		{ Object.values( options ).map( ( option, index ) => {
			return ( <CheckboxControl
				key={ option + index }
				label={ option[ 'label' ] }	
				checked={ metaFieldValue?.includes( option['value'] ) }
				value={ option[ 'value' ] }
				onChange={ ( value ) => updateMetaValue( option[ 'value' ], value ) }
			/> )
		} ) }
	</div> );
} );

registerPlugin( 'my-facebook-conversions-api-meta-box', {
	icon: '',
	render: () => {
		const postType = useSelect(
			( select ) => select( 'core/editor' ).getCurrentPostType(),
			[]
		);
		
		// ignore post types other than page
		if ( postType !== 'page' ) {
			return null;
		}
		
		return ( <PluginDocumentSettingPanel
			name="my-facebook-conversions-api-panel"
			title={ __( 'Facebook Conversions', 'my-facebook-conversions-api' ) }
			className="my-facebook-conversions-api-panel"
		>
			<ConversionTypeControl />
		</PluginDocumentSettingPanel> );
	},
} );
Code language: JavaScript (javascript)

Basically, if you already added custom controls to blocks or created an own block, adding a panel to the main document settings is not a big deal. If you know how-to. I needed quite some time to find the correct parts, especially with registerPlugin and PluginDocumentSettingPanel, so hopefully you don’t need to search too long and found my post early. 🙂

Leave a Reply

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