Overview

In this recipe, we’re using block filters to sprinkle in a custom attribute and controls that let editors add notes to any block. We’ll also add a dash of custom CSS in the Block Editor to visually highlight blocks with notes—making them easy to spot in the mix. It’s a handy trick to keep your content organized and your workflow seasoned to perfection! 📝✨👨‍🍳

Setup

You can choose to either use the repository which provides a development environment or to just download the standalone plugin

Standalone

Instructions

Run the following command in a terminal of your choice from inside the plugins directory of your local WordPress installation.

Zsh
npx @wordpress/create-block@latest editorial-notes --template @block-developer-cookbook/editorial-notes

Once the scaffold has completed completed, start the build process from inside the newly created plugin

Zsh
cd editorial-notes && npm run start

Finally, make sure to activate the plugin.

Repository

Instructions

Checkout the repository (skip this step if already done)

Zsh
git clone git@github.com:ryanwelcher/block-developer-cookbook.git

Install the dependencies

Zsh
npm install

Start the development environment (make sure you have Docker installed)

Zsh
npm run env start

Run the following script from the root of the repository

Zsh
npm run prep:editorial-notes

Once the scaffold has completed completed, start the build process from inside the newly created plugin

Zsh
cd plugins/editorial-notes && npm run start

Step 1 – Configure the build process

We’re not building any blocks in this recipe so let’s tell @wordpress/scripts where to find the file we want to use.

Update the start and build commands in the package.json file as follows to point to the src/notes.js file.

JSON
"start": "wp-scripts start src/notes-field.js",
"build": "wp-scripts build src/notes-field.js",

Now, start the build process but running the following in the terminal

Zsh
npm run start

Step 2 – Enqueueing the assets we need

We’re now building files but we need to enqueue the generated JavaScript and CSS files before we can start adding our custom items.

To do this we’re going to use two hooks, enqueue_block_editor_assets and enqueue_block_assets

Add the first hook to the main PHP in the root directory:

PHP
/**
 * Enqueue the JS containing our filters
 */
function editorial_notes_enqueue_scripts() {
	$notes_field_file = plugin_dir_path( __FILE__ ) . '/build/notes-field.asset.php';

	if ( file_exists( $notes_field_file ) ) {
		$assets = include $notes_field_file;

		// Enqueue the JavaScript that contains our filters.
		wp_enqueue_script(
			'notes-field',
			plugin_dir_url( __FILE__ ) . '/build/notes-field.js',
			$assets['dependencies'],
			$assets['version'],
			true
		);
	}
}
add_action( 'enqueue_block_editor_assets', __NAMESPACE__ . '\editorial_notes_enqueue_scripts' );

This code is checking for and enqueueing the JavaScript file we need using the enqueue_block_editor_assets hook to ensure it’s only being added to the block editor.

Add the next hook to the main file to enqueue the CSS:

PHP
/**
 * Enqueue the editor only style
 */
function editorial_notes_enqueue_block_editor_css() {
	$notes_field_file = plugin_dir_path( __FILE__ ) . '/build/notes-field.asset.php';
	if ( file_exists( $notes_field_file ) ) {
		$assets = include $notes_field_file;
		// Enqueue the CSS for the has-notes class.
		wp_enqueue_style(
			'notes-class',
			plugin_dir_url( __FILE__ ) . '/build/notes-field.css',
			array(),
			$assets['version'],
		);
	}
}
add_action( 'enqueue_block_assets', __NAMESPACE__ . '\editorial_notes_enqueue_block_editor_css' );

We’re using a different hook here because our CSS is styling the content of the block editor and not the UI of the block editorl

Step 3 – Adding a custom attribute to all blocks

The first part of our recipe is to add a custom notes attribute to each registered block that will hold any added notes from the editor.

We’re going to use a Block Filter called blocks.registerBlockType filter to do this. This filter allows us to modify the settings for a block at the point of registration.

The callback for this filter will receive two parameters; block settings and block name. 

Add the following callback to .src/notes-field.js

JavaScript
/**
 * Filter the block settings to add the notes attribute.
 *
 * @param {Object} settings Settings for the block.
 * @param {string} name     The name of the block.
 *
 * @return {Object} he modified settings.
 */
function addNotesAttribute( settings, name ) {
   return {
       ...settings,
       attributes: {
           ...settings.attributes,
           notes: {
               type: 'string',
               default: '',
           },
       },
   };
}

This code is taking the existing settings object and updating the attribute property to include a new attribute called notes of type string with a default value of an empty string.

Next, let’s register the filter for this:

JavaScript
addFilter(
   'blocks.registerBlockType',
   'block-developer-cookbook/notes-field',
   addNotesAttribute
);

addFilter is part of the @wordpress/hooks and take four parameters; the name of the filter, a unique namespace, the callback function, and finally an optional priority.

Your new attribute is registered! Now let’s add a way to interact with it.

Step 4 – Adding a control to manage the notes

In the previous step we registered a custom attribute so now we need to create a control to allow updating it.

To do this, we’re going to use another block filter called editor.BlockEdit which gives us access to the Edit component for a block.

Register the new filter and add the callback:

JavaScript
/**
 * Add a custom control to the block inspector controls for every block.
 *
 * @param {WPElement} BlockEdit The original block edit component.
 *
 * @return {WPElement} Element to render.
 */
function addEditorNotesField( BlockEdit ) {
   return ( props ) => {
       return (
           <>
               <BlockEdit { ...props } />
           </>
       );
   };
}
addFilter(
   'editor.BlockEdit',
   'block-developer-cookbook/notes-field-control',
   addEditorNotesField
);

There is a lot going on here so let’s take a minute to walk through it. The callback for this filter receives a single parameter which is the Edit component for the block being filtered. 

addEditorNotesField returns another function that receives the props from the Edit component as a parameter and the output of this will be used at the new Edit component for the block.

Finally , the function that is returned by addEditorNotesField returns the new JSX for the Edit component.

Notice the <BlockEdit { ...props } /> line in there? This component contains any existing controls from the original block.

Now, let’s add our new control below the existing controls. To do this we need to use the InspectorControls component.

Let’s add that now along with the PanelBody and TextareaControl components:

JavaScript
/**
* Add a custom control to the block inspector controls for every block.
*
* @param {WPElement} BlockEdit The original block edit component.
*
* @return {WPElement} Element to render.
*/
function addEditorNotesField( BlockEdit ) {
   return ( props ) => {
       const {
           attributes: { notes },
           setAttributes
       } = props;
       return (
           <>
               <BlockEdit { ...props } />
               <InspectorControls>
                   <PanelBody>
                       <TextareaControl
                           label={ __(
                               'Editorial Notes',
                               'block-developer-cookbook'
                           ) }
                           value={ notes }
                           onChange={ ( notes ) =>
                               setAttributes( { notes } )
                           }
                           help={ __(
                               'Add some editorial notes for this block'
                           ) }
                       />
                   </PanelBody>
               </InspectorControls>
           </>
       );
   };
}


addFilter(
   'editor.BlockEdit',
   'block-developer-cookbook/notes-field-control',
   addEditorNotesField
);

We’ve also added all of the settings for the TextareaControl to display and update the notes attribute.

Next, we’re going to add some code to make this run a little faster.

We only need this new control when the block is selected so let’s only display it when that is the case using the isSelected property from props.

JavaScript
/**
 * Add a custom control to the block inspector controls for every block.
 *
 * @param {WPElement} BlockEdit The original block edit component.
 *
 * @return {WPElement} Element to render.
 */
function addEditorNotesField( BlockEdit ) {
   return ( props ) => {
       const {
           attributes: { notes },
           setAttributes,
           isSelected
       } = props;
       return (
           <>
               <BlockEdit { ...props } />
               { isSelected && (
                   <InspectorControls>
                       <PanelBody>
                           <TextareaControl
                               label={ __(
                                   'Editorial Notes',
                                   'block-developer-cookbook'
                               ) }
                               value={ notes }
                               onChange={ ( newNotes ) =>
                                   setAttributes( { notes: newNotes } )
                               }
                               help={ __(
                                   'Add some editorial notes for this block'
                               ) }
                           />
                       </PanelBody>
                   </InspectorControls>
               ) }
           </>
       );
   };
}

addFilter(
   'editor.BlockEdit',
   'block-developer-cookbook/notes-field-control',
   addEditorNotesField
);

Step 5 – Displaying when a block has notes.

The last thing we need to do is show a visual cue that a block has notes inside the block editor. For this we’re going to use the editor.BlockListBlock.

This filter is used to modify the blocks’ wrapper component containing the block’s edit component and all toolbars. It receives the original BlockListBlock component and returns a new wrapped component.

JavaScript
/**
 * Add a custom class and CSS to show when a block has notes.
 *
 * @param {WPElement} BlockListBlock The original block list block component.
 *
 * @return {WPElement} Element to render.
 */
function addNotesDisplayClass( BlockListBlock ) {
   return ( props ) => {
       const { notes } = props.attributes;
       return (
           <BlockListBlock
               { ...props }
               className={ notes.length ? 'has-notes' : '' }
           />
       );
   };
}

addFilter(
   'editor.BlockListBlock',
   'block-developer-cookbook/notes-field-class',
   addNotesDisplayClass
);

In the callback for this filter, we are checking to see of the notes attribute has a length and if so, adding the has-notes class.

Once this class has been added, the CSS enqueued in Step 2 takes over and we can now see which blocks have notes.

Save and try it out!