Creating a Gutenberg Custom Block
After working on mostly Drupal projects for the past two years, I’m getting caught back up on the WordPress side of things and digging into Gutenberg and the Full Site Editor. I’ve built custom blocks with ACF in the past, and while those are great, I’ve been trying to learn how to get blocks working with JavaScript.
For my first block, I needed a hero banner that would display the post title overlaying the featured image, similar to the Cover block but without the editor needing to (re)enter the title and image.
I spun up a starting custom block with npx use @wordpress/create-block
, which gave me
a new plugin with build scripts included. There’s a lot that’s bundled in here (as I discovered
the hard way when I forgot to delete the node_modules
folder before copying the plugin
to a second project only for the copy to take over twenty minutes), but it was a quick and
painless way to scaffold a new block.
Next I edited the block.json
file so that the new block would support wide and full layout
options:
"supports": {
"html": false,
"align": [ "wide", "full" ]
}
I also added an attribute, isLink
, that would indicate whether the banner should link to
the post.
"attributes": {
"isLink": {
"type": "boolean",
"default": false
}
}
In my src/index.js
file, in addition to registering the block type, which was already
scaffolded, I registered an additional style as well. I wanted to create a smaller
version of the banner that could be used as a post teaser in a query loop.
registerBlockStyle('myplugin-banner/myplugin-banner', {
name: 'small',
label: 'Small'
});
With this in place, I needed to create the block markup when editing and saving.
WordPress already had a Post Title block and Post Featured Image block, so all I
really needed was a block that would wrap around those, add some additional markup,
and make the title and/or featured image (I ended up going with just the featured image)
link to the post when the parent block’s isLink
attribute was true. I also
needed to add the control itself so that the editor could toggle isLink
on and off.
The best resource for figuring out how to do most of this was looking at the block-library
for core blocks as well as the WordPress documentation. To create a block that included
a nested featured image and post title block, I needed InnerBlocks
from @wordpress/block-editor
.
const ALLOWED_BLOCKS = ['core/post-featured-image', 'core/post-title'];
export default function Edit() {
return (
<div {...useBlockProps()}>
<InnerBlocks
allowedBlocks={ ALLOWED_BLOCKS }
template={[
[ 'core/post-featured-image', {} ],
[ 'core/post-title', {'level': 1} ],
]}
templateLock='all'
/>
</div>
)
}
allowedBlocks
sets what block types are allowed to be nested in the block.
template
sets what blocks, and optionally, what attributes on those blocks, are
added whenever a new parent block is added. templateLock='all'
prevents nested
blocks from being added, moved, or removed.
With that, I could add CSS to my src/style.scss
to overlay the title on the
featured image.
To add the link functionality, I needed the InspectorControls
component from
@wordpress/block-editor
to add a new control to the sidebar. I also needed
PanelBody
and ToggleControl
from @wordpress/components
so I could reuse
the existing WordPress UI. ToggleControl
is a React component that accepts a
custom onChange
function. The simplest is to have it call setAttributes()
to
update a block attribute. attributes
and setAttributes
seem to work like any
React useState
hook.
Following a comment in a helpful GitHub thread, I learned that @wordpress/data
manages state similar to Redux and can be used to dispatch an update to an inner block.
const featuredImage = select( 'core/block-editor' )
.getBlocksByClientId(clientId)[0]
.innerBlocks[0];
gets the Block Editor’s Data, which then gets an array of block objects for the current client ID (so an array of one object, the current block). The block object’s innerBlocks property is an array of block objects nested within the current block. Since my template is locked, I assumed that the first inner block would always be the featured image block.
dispatch('core/block-editor')
.updateBlockAttributes(featuredImage.clientId, {
isLink: isLinkNow
});
then dispatches to the block editor’s data an action to update the inner block
found, setting its isLink
attribute to the new value.
Altogether then, my edit function looked like this:
export default function Edit({
attributes: {isLink},
setAttributes,
clientId
}) {
const onChangeLink = () => {
const isLinkNow = !isLink;
setAttributes( { isLink: isLinkNow });
const featuredImage = select( 'core/block-editor' )
.getBlocksByClientId(clientId)[0]
.innerBlocks[0];
dispatch('core/block-editor')
.updateBlockAttributes(featuredImage.clientId, {
isLink: isLinkNow
});
};
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Link settings' ) }>
<ToggleControl
label={ __( 'Make banner a link' ) }
onChange={ onChangeLink }
checked={ isLink }
/>
</PanelBody>
</InspectorControls>
<div {...useBlockProps()}>
<InnerBlocks
allowedBlocks={ ALLOWED_BLOCKS }
template={[
[ 'core/post-featured-image', {} ],
[ 'core/post-title', {'level': 1} ],
]}
templateLock='all'
/>
</div>
</>
);
}
My save.js
was much easier. All that needed to do was render the InnerBlock’s
content.
export default function save() {
return (
<div {...useBlockProps.save()}>
<InnerBlocks.Content />
</div>
);
}
With that in place, I had a working block I could use and add to a post type’s template.
References
- https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/nested-blocks-inner-blocks/
- https://github.com/WordPress/gutenberg/issues/11873
- https://github.com/TomodomoCo/gutenberg-block-columns/blob/master/src/block/inspector.js#L53
- https://developer.wordpress.org/block-editor/reference-guides/data/data-core-block-editor/