Block editor: Add settings to document sidebar
Published: – Leave a comment
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. 🙂