WordPress.org

WordPress Developer Blog

Setting up a multi-block plugin using InnerBlocks and post meta

Setting up a multi-block plugin using InnerBlocks and post meta

WordPress has always been incredibly flexible and customizable. The Block and Site Editors are expanding our abilities to create custom experiences, and the ability to create custom blocks only enhances that ability.

Currently, the create-block utility only allows for creating one block at a time and doesn’t give the option for a multi-block setup. For example, what if you want to create two or more related blocks, especially where one custom block would use another to curate a user’s experience? What if you aim to use post meta to provide extra context to a user? These are common use cases and the area in which WordPress has always excelled. 

Below, you’ll learn how to structure a multi-block approach using the create-block utility.

Let’s keep that going by setting up the ability to review a post or custom post type (CPT) with a rating system and include that in another custom block that can be used in a Query Loop. This could be used with your content when creators publish reviews (books, movies, products) and need a rating system to display with their posts. The end result should look like this:

Specifically, you’ll build:

  1. A Review Card block with three specific inner blocks:
    1. Title (core/post-title)
    2. Rating block (a block we’ll make as well)
    3. Excerpt (core/post-excerpt)
  2. A Rating block that will:
    1. Rate a post (or CPT) from one to five stars or hearts
    2. Use post meta to store the rating
    3. Use the block in the Post Query block to show the rating of each post.

You’ll need a local environment set up and a terminal with NodeJS installed to run commands. If you don’t already have a local environment set up, you can use wp-now, LocalWP, or another option listed here that would suit your needs.

If you would like to follow along with the finished code, it is available in this Github repo and will be updated as needed; PRs welcome!

Setting up a multi-block plugin

Let’s start with setting up our project structure. In your terminal, cd into your wp-content/plugins directory. If you’re using wp-now you can work from whichever directory you like.

Scaffold the plugin files

Run npx @wordpress/create-block@latest post-review-blocks. This will scaffold a block plugin called “Post Review Blocks” in a directory named post-review-blocks. Go into that directory and open it in your code editor of choice. You should see a post-review-blocks.php file. Open that, and your code should look like the following (without the default comments):

/**
 * Plugin Name:       Post Review Blocks
 * Description:       Example block scaffolded with Create Block tool.
 * Requires at least: 6.1
 * Requires PHP:      7.0
 * Version:           0.1.0
 * Author:            The WordPress Contributors
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       post-review-blocks
 *
 * @package CreateBlock
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

function create_block_post_review_blocks_block_init() {
	register_block_type( __DIR__ . '/build );
}
add_action( 'init', 'create_block_post_review_blocks_block_init' );

The plugin itself should have a file structure like this:

- build/
- src/
- .editorconfig
- .gitignore
- .package-lock.json
- .package.json
- post-review-blocks.php
- readme.md

You will be working in the post-review-blocks.php file and src directory for this tutorial, and the build directory will get built automatically.

Scaffold the blocks

Next, delete all the files in the src directory so that the src is empty, and cd into the src if you’re not already there.

Run the following two commands:

npx @wordpress/create-block@latest review-card-block --no-plugin
npx @wordpress/create-block@latest rating-block --no-plugin --variant dynamic

This will create the two custom blocks needed for our multi-block setup. Note the  --no-plugin to both commands. The flag indicates that this is just creating the block files, not all the plugin files needed. Also, you can see the rating-block will be a “dynamic” block, rendering with PHP. Why? This allows you to get practice with both static and dynamic blocks.

Now, there are two blocks in our src folder:

  • rating-block
  • review-card-block

You can take care of a few more things:

  • Go into the block.json file for the rating-block and change the “icon” property from “smiley” to “star-filled”.
  • In both of the block.json files for each block, add the “keywords” property with "keywords": ["rating", "review"]. Your users will find the new blocks easier when searching.  
  • In the post-review-blocks.php file, update create_block_post_review_blocks_block_init to register both blocks, like this:
function create_block_post_review_blocks_block_init() {
	register_block_type( __DIR__ . '/build/rating-block' );
	register_block_type( __DIR__ . '/build/review-card-block' );
}
add_action( 'init', 'create_block_post_review_blocks_block_init' );

Build the blocks and activate the plugin

Now, from the root of the post-review-blocks plugin (the “root” is at the same level as the package.json file), run npm start,and the blocks should build into their own sub-directories in build. You can leave this script running for the remainder of the tutorial. Alternatively, you can stop and restart it if you want to run other commands or when you make changes to files like block.json. The script needs to be running for any changes to appear in the editor.

Once the build is successful, activate the plugin either in the WordPress dashboard or via WP-CLI with wp plugin activate post-review-blocks.

At this point, you can check that the blocks are registering by creating a new post and checking the inserter for the blocks by typing in one of the keywords we used:

Success! 🎉

You now know how to set up the structure of a multi-block plugin. You can add new blocks to the src folder with create-block and register their generated scripts in the build directory.

Now it’s time to add post meta functionality, assign inner blocks, and limit where the blocks can be used.

Registering post meta

Open up the post-review-blocks.php again, and paste the following after the create_block_post_review_blocks_block_init function:

// Add some post meta
function register_review_rating_post_meta() {
	$post_meta = array(
		'_rating'      => array( 'type' => 'integer'	),
		'_ratingStyle' => array( 'type' => 'string'	),
	);

	foreach ( $post_meta as $meta_key => $args ) {
		register_post_meta(
			'post',
			$meta_key,
			array(
				'show_in_rest'  => true,
				'single'        => true,
				'type'          => $args['type'],
				'auth_callback' => function() {
					return current_user_can( 'edit_posts' );
				}
			)
		);
	}
}
add_action( 'init', 'register_review_rating_post_meta' );

This function registers two post meta keys you will need:

  • _rating
  • _ratingStyle

These need to be registered. Otherwise, the data won’t be saved when you update our Rating block. You’ll also notice the two meta keys are prefixed with an underscore: _. This “protects” the meta from being used in the post’s custom fields section and potentially overwritten by the value in that meta box.

Finally, note that show_in_rest is set to true, and the auth_callback checks to make sure the user has at least edit_posts privileges. If the meta does not show up in the WordPress REST API, it cannot be edited in the block editor. 

A quick note beyond the scope of this tutorial: Be cautious with the data shown in the REST API. If you need to filter data out of the public API, do so in PHP elsewhere or choose a different method for working with sensitive data.

Building the Rating block

The Rating block allows your users to rate a post on a scale from one to five and choose between displaying a star emoji (⭐) or a heart emoji (❤️). You can copy these emojis into your code. With this functionality, the block accomplishes two objectives:

  1. Demonstrate how to save and pull from post meta
  2. Allow the post meta to be used in a Query Loop Post Template

There are many applications for this kind of functionality with CPTs, including:

  • Staff directories with email or position meta
  • Book catalogs with rating or ISBN meta
  • Recipe indexes with tastiness or prep_time meta

The possibilities are numerous!

Ok, all the edits in this section will be within the src/rating-block directory.

Add basic CSS

The following CSS is for the styles.scss file. Open that file, remove any CSS in there, and paste in the following, which adds padding around the block and ensures the star is yellow and the heart is red.

.wp-block-create-block-rating-block {
	padding: 1rem 0;
}

.wp-block-create-block-rating-block span.rating-star {
	color: yellow;
}

.wp-block-create-block-rating-block span.rating-heart {
	color: red;
}

Adjust these to your liking and save the file.

Add attributes and usesContext to the block.json file

Open up the block.json file for the Rating block and add the following properties:

"usesContext": ["postId", "postType"],
"attributes": {
	"rating": {
		"type": "integer",
		"default": 5
	},
	"ratingStyle": {
		"type": "string",
		"default": "star"
	}
},
"example": {
	"attributes": {
		"rating": 4,
		"ratingStyle": "star"
	}
}

The JSON above does the following:

  • usesContext: allows us to get the values of the post’s ID and type
  • attributes: identifies the properties to be saved on the block
  • example: gives a preview of what the block could look like when added

Click here to see the final block.json file.

Update the edit.js file

There is a lot to tackle. You can break it into three sections: 

  • Import and assignments
  • The functionality of retrieving and storing the post meta
  • Return method with the components in the editor sidebar
     

The full file can be found on GitHub.

Delete what is currently in the file. Then, at the top of the edit.js file, add the following:

import { __ } from "@wordpress/i18n";
import { useEffect } from "@wordpress/element";
import { useBlockProps, InspectorControls } from "@wordpress/block-editor";
import { PanelBody, RangeControl, SelectControl } from "@wordpress/components";
import { useEntityProp } from "@wordpress/core-data";

import "./editor.scss";

This section sets up our imports:

  • __ is our internationalization method
  • useEffect allows you to update metadata
  • useBlockProps gives you the block properties to work with
  • InspectorControls allows you to add controls to the Inspector sidebar
  • PanelBody, RangeControl, and SelectControl are all components to set up the user controls for the properties of the block
  • useEntityProp provides access to the post meta
  • And finally, the SCSS file is imported

Next, add the following: 

export default function Edit( {
	attributes: { rating, ratingStyle },
	setAttributes,
	context: { postType, postId },
} ) {
	const [meta, updateMeta] = useEntityProp(
		"postType",
		postType,
		"meta",
		postId,
	);

	// Add functionality code here

	// Add return() method here
	
	// Other code will go here, don't forget or delete the closing curly brace!
}

This is the Edit method, which controls what shows up in the block editor. First, you pass in and assign the following:

  • rating and ratingStyle get passed in and assigned to the attributes state object
  • setAttributes is the method by which the attributes state object gets updated
  • postType and postId are passed in with context from the usesContext you defined above

Next, you see that the state object for meta and the updateMeta method are getting assigned from the useEntityProp method, which is used by the block to get or change meta values.

Ok, so far so good? Now for the functionality! 

In place of the “Add functionality code here” comment, add the following methods:


useEffect(() => {
    const initStyle = meta?._ratingStyle ? meta?._ratingStyle : "star";
    setAttributes({
        rating: meta?._rating || 0,
        ratingStyle: initStyle,
    });
}, []);
const onChangeRating = (val) => {
    updateMeta({
        ...meta,
        _rating: val,
    });
    setAttributes({
        rating: val
    });
};
const onChangeRatingStyle = (val) => {
    updateMeta({
        ...meta,
        _ratingStyle: val,
    });
    setAttributes({
        ratingStyle: val
    });
};
const getRatingEmojis = () => {
    let ratingEmojis = "";
    for (let i = 0; i < rating; i++) {
        ratingEmojis += ratingStyle === "star" ? "⭐" : "❤️";
    }
    return ratingEmojis;
};

What do each of these methods do?:

  • useEffect runs on load (given the second parameter is []) and checks for and assigns values from the stored post meta for rating and ratingStyle, defaulting to the star (⭐)
  • onChangeRating will update the value of the rating when it’s changed in the Inspector
  • onChangeRatingStyle does the same for the ratingStyle
  • getRatingEmojis is a loop that returns the correct number of and style of stars or hearts for the rating.

Finally, add the following in place of the comment “Add return() method here”:

return (
    <>
        <InspectorControls>
            <PanelBody title={ __( "Rating", "multiblock-plugin" ) }>
                <RangeControl
                    label={ __( "Rating", "multiblock-plugin") }
                    value={ rating }
                    onChange={ onChangeRating }
                    min={ 1 }
                    max={ 5 }
		/>
                <SelectControl
                    label={ __( "Rating Style", "multiblock-plugin" ) }
                    onChange={ onChangeRatingStyle }
                    value={ ratingStyle }
                    options={ [
                        {
                            label: __( "Star" , "multiblock-plugin" ),
                            value: "star",
                        },
                        {
                            label: __( "Heart", "multiblock-plugin" ),
                            value: "heart",
                        },
                    ] }
                />
            </PanelBody>
        </InspectorControls>
        <div { ...useBlockProps() }>
            <p>
                <strong>Rating:</strong>{ " " }
                <span className={ `rating-${ratingStyle}` }> { getRatingEmojis() }</span>
            </p>
        </div>
    </>
);

The return function here has two main parts: the InspectorControls and the actual output of the block for the editor.

Within InspectorControls, we create a PanelBody that contains a RangeControl and a SelectControl. The RangeControl determines the value one to five of the ratings given to the post or CPT. The SelectControl determines whether to show star or heart emojis.

The final div is the output where we show the label “Rating:” and then pass in the correct style and number of stars or hearts.

Once you save this and add the block to a post, you should see the following:

If you slide the range slider or switch between hearts and stars, you should see the stars or hearts update in real time.

For more info on adding and using post meta, check out this great tutorial.

Update the render.php file

Since you created this block as a dynamic block, you need to implement how the rating will display on the front end. You can delete the code in the render.php file and add the following:

<?php
global $post;

$ratingEmojis = '';

// Get the rating and rating style from the post meta
$rating      = get_post_meta( get_the_ID(), '_rating', true );
$ratingStyle = get_post_meta( get_the_ID(), '_ratingStyle', true );

// If the rating style is not set, default to star
if ( ! $ratingStyle ) {
    $ratingStyle = 'star';
}
// Generate the rating emojis.
for ( $i = 0; $i < $rating; $i++ ) {
    $ratingEmojis .= $ratingStyle === 'star' ? '⭐️' : '❤️';
}

?>
<p <?php echo get_block_wrapper_attributes(); ?>>
		<?php echo wp_kses_post( '<strong>Rating:</strong> <span class="rating-' . $ratingStyle . '">' . $ratingEmojis . '</span>', 'multiblock-plugin' ); ?>
</p>

A couple of things to note here:

  • This comes in the context of a post, so we can use get_the_ID()
  • The block gets wrapped with classes and other attributes with get_block_wrapper_attributes()

This means you now have a functional Rating block on the front end as well:

Fantastic! 

If you would like to check your work, you can see the final file on GitHub. Otherwise, it’s time to move on to the next block in our multi-block setup.

Building the Review Card block

This block will involve a lot less code and is more about configuration. We want to do a few things with this block:

  • Assign specific blocks as “inner blocks” to the Review Card
  • Only allow certain blocks within the Review Card
  • Limit where this card can be used, namely, as a post template in the Query Loop block

There are no styles needed for this block, so delete the contents of the .scss files or style them to your liking.

All the code in this section will be edited in the src/review-card-block directory.

Updating the block.json file

You need to start by setting a few properties in the block.json file. Open that up and add the following:

"parent": ["core/post-template"],
"allowedBlocks": [
	"core/post-title",
	"create-block/rating-block",
	"core/post-excerpt"
]

First, add a parent block property indicating that the Review Card can only be used in the core/post-template block. Second, only three blocks are allowed to be used in the Review Card block itself. You will see this in action below.

If needed, compare your settings here with the file in the Github repo.

InnerBlocks in edit.js

In the edit.js file, replace all the code with the following:

import { __ } from "@wordpress/i18n";
import { InnerBlocks, useBlockProps } from "@wordpress/block-editor";
import "./editor.scss";

const REVIEW_TEMPLATE = [
	[ "core/post-title", { isLink: true, placeholder: "The Post Title" } ],
	[ "create-block/rating-block", {} ],
	[ "core/post-excerpt", { placeholder: "The pose content..." } ],
];

export default function Edit() {
	return <InnerBlocks template={REVIEW_TEMPLATE} templateLock="all" />;
}

The main difference in this file is that you are importing InnerBlocks and assigning a template of blocks as inner blocks to the Review Card block.

Next, REVIEW_TEMPLATE is an array of arrays, the first index being the block you want to use and the second being an object with any configuration you wish to pass through. For example, you can see the core/post-title block is taking in two defaults:

  • isLink is set to true, meaning the title is clickable and will take you through to the post
  • placeholder if, for some reason, there’s no title

Then, you can see that the InnerBlocks component is used in the Edit function, passing in the REVIEW_TEMPLATE and also locking the blocks. This is just to show you how to lock the blocks in the template if the situation calls for it, but it is not required.

Edit the save.js file

Since the post-review-block is a static block, you’ll need to edit the save.js file to allow saving the block to the database. Open up that file, delete the contents, and add the following:

import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';

export default function save( { attributes } ) {
	return (
		<div { ...useBlockProps.save() }>
			<InnerBlocks.Content />
		</div>
	);
}

In this case, since the InnerBlocks are set up and used, all that is needed is to pass in InnerBlocks.Content. After saving your file, return to the block editor.

Testing the blocks

If you haven’t already, create two or three posts with different ratings for each post using the new ratings block. Publish those posts.

Then, open a new page in the dashboard and add a title for the page. “A review of posts…” is what is in the screenshots below. Next, add a Query Loop block. Select Start Blank and then pick an option like Title and Date

Open the List View, and you should see something like this:

Now:

  • Click on the Title block in the Post Template so that it is highlighted
  • Then click on the Inserter (the blue “+” button) and search for Review Card block
  • Once you see it in the list, click on it and go back to the List View, where you should see the Review Card block 

Save or publish the page. 

You will notice that no rating is showing up in the block editor. This is because the Rating block is rendered with PHP and is built dynamically, so a refresh is required. On refreshing the page, you should see the different reviews for the different posts:

Awesome!

One thing to note, if you open the List View, you will see the locks on the inner blocks of the Review Card block. These were set earlier with templateLock="all" and cannot be moved around or deleted.

Similarly, if you try moving the Review Card block out of the Post Template block, you will see that it is also not allowed since the block requires its parent to be a Post Template block.

Excellent! You now have a working multi-block plugin that uses post meta, InnerBlocks, and is explicit about which blocks are allowed within the InnerBlocks. Nice work!

One more thing to cover, and you should be on your way to block customization bliss.

Limiting the usage of the Rating block

You may have already been thinking this, but the current setup of the Rating block should really be limited to use on posts or other CPTs. To achieve this, open up the post-review-blocks.php one more time and add the following:

function limit_rating_block_to_post_type( $allowed_block_types, $editor_context ) {
	// Only allow paragraphs, headings, lists, and the rating block in the post editor for Posts.
	if ( 'post' === $editor_context->post->post_type ) {
		return array(
			'core/paragraph',
			'core/heading',
			'core/list',
			'create-block/rating-block'
		);
	}

	return $allowed_block_types;
}
add_filter( 'allowed_block_types_all', 'limit_rating_block_to_post_type', 10, 2 );

With this filter, you can specify when and where the Rating block will be available to insert. In this case, the code gives the example that only Paragraph, Heading, List, and Rating blocks can be used in posts. If you open a new post and click on the blue + inserter or type “/” to insert a block, you will see the options are now very limited:

This is something to consider, especially for CPTs where you may only want a limited number of core or custom blocks to show up as options for users.

Wrapping up

There is a lot to consider when building multiple custom blocks. Hopefully, this tutorial has helped organize some approaches and given you a multi-block blueprint for moving forward with your own customizations.

This is the repository with the code if you would like to clone it or fork it for your needs.

Good luck and have fun!

Resources to learn more 

These are some helpful resources used to develop this tutorial. Explore these to deepen your block knowledge:

Props @bph, @rmartinezduque , @ndiego, and @welcher for proofreading and all the helpful suggestions!

8 responses to “Setting up a multi-block plugin using InnerBlocks and post meta”

  1. Creative Andrew Avatar

    Really good article Nate! Thanks for sharing

    1. Nate Finch Avatar

      Thanks for the feedback, Andrew, glad you found it useful 🙂.

  2. Mrinal Haque Avatar

    Thanks for this fantastic article. But, here is a typo-
    ✗ npx wordpress/create-block@latest review-card-block –no-plugin
    ✓npx wordpress/create-block@latest review-card-block -–no-plugin

    ✗ npx wordpress/create-block@latest rating-block –no-plugin –variant dynamic
    ✓npx wordpress/create-block@latest rating-block -–no-plugin –variant dynamic

    1. Mrinal Haque Avatar

      Sorry, I also typed the wrong command
      ✗ npx wordpress/create-block@latest rating-block –no-plugin –variant dynamic
      ✓npx wordpress/create-block@latest rating-block -–no-plugin –-variant dynamic

      1. Mrinal Haque Avatar

        Strange!!! npx wordpress is automatically changing to wordpress.

        1. Nate Finch Avatar

          Thanks Mrinal! I at least got those commands updated and consistent. 🙂

  3. Lovro Hrust Avatar

    I used to copy block files in additional subfolders from first scaffolded block and was not aware of –no-plugin option. Thanks for this!

    1. Nate Finch Avatar

      You got it, Lovro, glad it was helpful 🙂!

Leave a Reply

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