Limiting the block count for InnerBlocks

One of the most versatile components that Gutenberg block developers have is the <InnerBlocks/> component. This component allows other blocks to be inserted into it. WordPress Core ships with the Group and Columns blocks that both use InnerBlocks internally to allow the creation of very complex block structures.

The InnerBlocks component solves some interesting problems for custom blocks, for example creating a list of ordered items. By leveraging InnerBlocks, one can skip having to build a bespoke system and rely on Gutenberg to handle the insertion, deletion, and ordering of items. It is very flexible and easy to use. However, while working with it I discovered that there is no built-in way to limit the number of blocks that can be inserted.

The renderAppender prop will allow us to solve this issue

RenderAppender Overview

Before we talk about the solution, let briefly discuss the renderAppender prop and how it works.

Released with WordPress. 5.3, the renderAppender prop allows us to control the interface for inserting blocks into the related <InnerBlocks/> component.

There are two predefined appenders available in WordPress core:

  1. <InnerBlocks.DefaultBlockAppender />
  2. <InnerBlocks.ButtonBlockAppender />

<InnerBlocks.DefaultBlockAppender />

As the name indicates, this is the default appender. If the renderAppender prop is undefined, then this is the appender that is used. It automatically inserts whichever block is configured as the default block via wp.blocks.setDefaultBlockName – which by default is the Paragraph block.

If you’ve worked in the block editor at all you’ve already seen this.

Look familiar?

So when working with <InnerBlocks/>, you don’t need to pass anything to renderAppender to get this style of appender. If fact, you can leave it off entirely.

<InnerBlocks />

<InnerBlocks.ButtonBlockAppender />

This appender is displayed as a + ( plus ) icon button that, when clicked, displays the block inserter. This appender does not insert a default block.

This appender is seen in the Group or Columns block.

Groups and Column blocks use InnerBlocks.ButtonBlockAppender

To use this in your <InnerBlocks/>, you have to pass a function to renderAppender that returns the <InnerBlocks.ButtonBlockAppender /> component.

<InnerBlocks
    renderAppender={ () => (
        <InnerBlocks.ButtonBlockAppender />
     ) }
/>

No appender at all

The last option is to pass false to renderAppender to not display any appender at all.

<InnerBlocks
    renderAppender={false}
/>

Solution

Now that we have an understanding of how the renderAppender prop works, let us use it to solve the problems of limiting the number of blocks allowed.

After some experimentation, the actual solution is fairly straight forward, we just need a way to count how many blocks have been inserted and if it’s more than we want, we set renderAppender to false so that nothing is displayed and the user cannot insert any more blocks until one is removed.

The core/block-editor datastore provides a getBlock() selector that we can use to get the information we need. We pass it the clientId of our block and we can get the list of innerBlocks, count them, and either display the appender or return false.

Below is the code that you would place into the edit property of a block, it’s very simple and but demonstrates the the solution.

import { useSelect } from '@wordpress/data';
import { InnerBlocks } from '@wordpress/block-editor';

const InnerBlockWithLimit = ( props ) => {
	const { clientId } = props;
	const innerBlockCount = useSelect( ( select ) => select( 'core/block-editor' ).getBlock( clientId ).innerBlocks );

	const appenderToUse = () => {
		if ( innerBlockCount.length < 10 ) {
			return (
				<InnerBlocks.ButtonBlockAppender/>
			);
		} else {
			return false;
		}
	}

	return (
		<InnerBlocks
			renderAppender={ () => appenderToUse() }
		/>
	)
}

Have you seen any other issues when working with <InnerBlocks/> or want to discuss this approach? If so, leave a comment below!

Leave a Reply

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