The Publishing Project

Building Gutenberg Blocks (Part 2)

We’ve only worked with static content so far. But the idea behind Gutenberg is to let you work with your own content.

This section will also introduce the RichText block components as opposed to the plain text elements we’ve been working with.

Editable Blocks

The first part of this process is to create an editable block where we can enter arbitrary data that will be saved when we save or publish the post.

IN addition to i18n and blocks imports we import the RichText component from the block-editor package. This will do the heavy load in the code below.

import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
import { RichText } from '@wordpress/block-editor';

The first difference is that we’ve now added an attributes attribute that will hold the structured data needs of a block. WordPress will also use this to validate the content of the block.

In this example, the content child of the attribute is an array of children that paragraphs (the children match the p selector).

registerBlockType( 'rivendellweb-blocks/example-03', {
    title: __( 'Example 03', 'rivendellweb-blocks' ),
    icon: 'universal-access-alt',
    category: 'layout',
    attributes: {
        content: {
            type: 'array',
            source: 'children',
            selector: 'p',
        },
    },

The edit and save methods get a little more complicated when we use custom data.

The save() method now takes props as an attribute.

We assign the content attribute, the setAttribute and className to props.

Next, we create an onChangeContent function that will trigger when we add or remove content from the element.

Finally, we return a RichText element that returns p elements with the className class and the content as the value of the element.

The onChange event firest with the onChangeContent every time the element changes.

    example: {},
    edit: ( props ) => {
        const {
            attributes: { content },
            setAttributes,
            className,
        } = props;

        const onChangeContent = ( newContent ) => {
            setAttributes( { content: newContent } );
        };

    return (
            <RichText
                tagName="p"
                className={ className }
                onChange={ onChangeContent }
                value={ content }
            />
        );
    },

The save() method also takes props as a parameter and uses props.attributes.content as the value of the value parameter.

    save: ( props ) => {
        return (
            <RichText.Content
                tagName="p"
                value={
                  props.attributes.content
        } />
        );
    },
} );

Inner blocks

The next idea is to see if we have any way to automate the content of the block. Right now our block is limited to whatever tag we use as the value of tagName.

We can create a single block with a list of default blocks using inner blocks.

We import InnerBlocks from @wordpress/block-editor and then use it as the child of our content div.

import { registerBlockType } from '@wordpress/blocks';
import { InnerBlocks } from '@wordpress/block-editor';

// Register block
registerBlockType( 'rivendellweb-blocks/example-04', {
    title: 'Example 04',
    category: 'rivendellweb-blocks',
    icon: 'translation',
    edit: ( { className } ) => {
        return (
            <div className={ className }>
                <InnerBlocks />
            </div>
        );
    },
    save: ( { className } ) => {
        return (
            <div className={ className }>
                <InnerBlocks.Content />
            </div>
        );
    },
} );

The idea is that using Inner Blocks we get a set of blocks available without any additional code.

Blocks with customized content content

Inner blocks give us a set of sensible default set of blocks to use but it may or may not be enough to suit our needs. Fortunately, we don’t have to stick with the defaults if we need specific blocks to accomplish our goals.

First we run all our imports as usual, __ for i18n, registerBlockType and InnerBlocks

import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
import { InnerBlocks } from '@wordpress/block-editor';

The first difference is the inclusion of an ALLOWED_BLOCKS constant that we’ll use later to tell Gutenberg what blocks to allow in our inner blocks.

const ALLOWED_BLOCKS = [
  'core/image',
  'core/heading',
  'core/paragraph'
];

The final change is in the edit method of registerBlockType where we add an allowedBlock attribute and use the ALLOWED_BLOCKS constant as the value.

registerBlockType( 'rivendellweb-blocks/example-05', {
  title: __('Example 05', 'rivendellweb-blocks'),
    category: 'rivendellweb-blocks',
    icon: 'translation',

  edit: ( { className } ) => {
    return (
      <div className={ className }>
        <InnerBlocks
          allowedBlocks = { ALLOWED_BLOCKS }/>
      </div>
    );
  },

  save: ( { className } ) => {
    return (
      <div className={ className }>
        <InnerBlocks.Content />
      </div>
    );
  },
} );

The idea is that we restrict the content of the InnerBlock element to whatever elements we want to use. This way we can be sure that users don’t modify the content in ways we don’t expect.

Block Templates

We can go even further and define a complete template for the block, not only limiting the blocks but providing a ready-to-complete template

The import elements don’t change. We import __, registerBlockType and InnerBlocks as before.

import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
import { InnerBlocks } from '@wordpress/block-editor';

Instead of an allowed blocks section, we create a BLOCK_TEMPLATE variable that lists the blocks we want to use, the order of the blocks and, where appropriate, placeholder text.

const BLOCK_TEMPLATE = [
  [ 'core/image', {} ],
  [ 'core/heading', { placeholder: 'Book Title' } ],
  [ 'core/heading', { placeholder: 'Book Author' } ],
  [ 'core/paragraph', { placeholder: 'Summary' } ],
];

The template attribute uses our BLOCK_TEMPLATE constant as the layout of the blocks we’re building

templateLocks allows locking the InnerBlocks area for the current template. It takes one of the following values:

  • all — prevents all operations. It is not possible to insert new blocks. Move existing blocks or delete them.
  • insert — prevents inserting or removing blocks, but allows moving existing ones.
  • false — prevents locking from being applied to an InnerBlocks area even if a parent block contains locking.

For example, our book block may want to do more than one paragraph summary or we may want to add more images. Or maybe not, the choice is up to the designer.

registerBlockType( 'rivendellweb-blocks/example-06', {
  title: __('Example 06', 'rivendellweb-blocks'),
    category: 'rivendellweb-blocks',
    icon: 'translation',
  edit: ( { className } ) => {
    return (
      <div className={ className }>
        <InnerBlocks
        template={ BLOCK_TEMPLATE }
        templateLock="false" />
      </div>
    );
  },

  save: ( { className } ) => {
    return (
      <div className={ className }>
        <InnerBlocks.Content />
      </div>
    );
  },
} );

We can build multicolumn blocks using core blocks and place them in a template. The following example creates a two-column layout with an image in the first column and one or more paragraphs in the second.

const TEMPLATE = [ 
  [ 'core/columns', {}, [
    [ 'core/column', {}, [
        [ 'core/image' ],
    ] ],
    [ 'core/column', {}, [
        [ 'core/paragraph', { placeholder: 'Enter side content...' } ],
    ] ],
  ] ] 
];

So now we have the flexibility of building composite blocks with pre-defined structure and content flow.