Anatomy of a Custom Block: How Gutenberg Blocks Work

If you’re a WordPress developer who is getting started writing custom content blocks for Gutenberg, you’ll find that this is a very different kind of development than you are used to doing.  Gutenberg relies heavily and JavaScript and React instead of PHP, and the underlying architecture is unique.  When I first started writing custom blocks for Gutenberg, I was copying and pasting from tutorials and code samples, and it took me a while to understand what was actually happening under the hood because it is so different from anything I have done before.  In this tutorial, I will explain the anatomy of a WordPress custom block.

When you create a block, the JavaScript file that builds your block has three main sections:

  1. A list of block attributes
  2. The edit function, which describes how the block looks in the editor
  3. The save function, which describes how the block looks on the front end

Here is a very simple example of a block, borrowed from WordPress’s Gutenberg handbook with a few modifications:

var el = wp.element.createElement,
    registerBlockType = wp.blocks.registerBlockType,
    blockStyle = { backgroundColor: '#900', color: '#fff', padding: '20px' };

registerBlockType( 'gutenberg-boilerplate-es5/hello-world-step-01', {
    // Section 1: list of block attributes
    title: 'Hello World (Step 1)',
    icon: 'universal-access-alt',
    category: 'layout',
    attributes: {
        title: {
                type: 'array',
                source: 'children',
                selector: 'h2'
            },

        text: {
                type: 'array',
                source: 'children',
                selector: '.text'
            },
    }

    // Section 2: the edit function
    edit: function() {
        return [
            el( 'div',
                el( blocks.Editable, {
                    tagName: 'h2',
                    value: attributes.title,
                } ),
               el( blocks.Editable, {
                     tagName: 'div',
                     value: attributes.text,
               } )
            )
         ];
    },

    // Section 3: the save function
    save: function() {
        return (
            el( 'div', 
                    attributes.title &&
                        el( 'h2', {}, attributes.title ),
                    attributes.text &&
                        el( 'div', { className: 'text' }, attributes.text),
                )
            );
    },
} );

To understand what these three sections are doing, you need to understand how Gutenberg works.

In WordPress today, the post content is a bunch of HTML that you can edit with TinyMCE, the WordPress editor.  You type your content into the TinyMCE box, format it, and it saves as HTML.  When you display the post content on the front end of the site, the post_content() function returns that HTML.

Gutenberg needs to return HTML too.  But Gutenberg also needs to have a lot more flexibility and complexity in the back end in order to make individual blocks configurable.  In the back end, blocks have options for colors and formatting, and some of them have form fields you can fill out. Some parts of blocks have URL inputs or image uploads.

Gutenberg has a clever way of handling the difference between the back-end form and the front-end display: the post content is saved as HTML just like always, but that HTML includes comments that tell Gutenberg how to build the back-end user interface.  In other words, that back-end user interface isn’t actually saved anywhere, but it is built on the fly as Gutenberg parses the HTML in the post content.

We’re used to building interfaces such as custom metaboxes where a user fills in a form field and that field is saved in a database with a unique key. We can access the content of the form field by querying the database for that unique key. Gutenberg works completely differently.  We might build several input fields into a block, but those inputs are not saved individually.  Instead, they are incorporated into the HTML of the post content.  When we want to access those inputs in Gutenberg, we don’t query the database for a unique key.  Instead, we tell Gutenberg where we saved that data in the HTML, and then we pull it out of the HTML to display and manipulate it in the back end.

Let’s look at the code in detail to see how this works. It actually makes sense to start with the last part of the code, the save function, because this is our end-goal, and everything else depends on it:

    save: function() {
        return (
            el( 'div', 
                    el( 'h2', {}, attributes.title ),
                    el( 'div', { className: 'text' }, attributes.text),
                )
            );
    },

This code says that this block needs to return a div containing an h2 and another div with a class of “text.”  If we are looking at an existing block (as opposed to a new one), the h2 will contain the title attribute, and the div.text will contain the text attribute. In other words, we are telling Gutenberg that our content should look like this:

<div>
    <h2>The title attribute</h2>
    <div class="text">The text attribute</div>
</div>

This is what will be saved in the post content.

Now, we can back up and look at the first part of our code, where we declare all of our attributes.  Now that we know the HTML structure of our block, we can work backwards just like Gutenberg does to build the block in the dashboard.

The first section of the code required to make a block declares everything needed to make the block: the title, icon, category, and attributes.  When users add a new block to their content, they will use the title and icon to find the block they want.  Gutenberg splits blocks into categories, so you need to declare which category this block belongs to.  The attributes are the most important and potentially confusing part of this section.

    attributes: {
        title: {
                type: 'array',
                source: 'children',
                selector: 'h2'
            },

        text: {
                type: 'array',
                source: 'children',
                selector: '.text'
            },
    }

Each attribute needs three things:

  1. Type: this is almost always going to be array, unless you are looking for a number or perhaps a string.
  2. Source: this is usually going to be children, because the source of this attribute is a child element of the block
  3. Selector: this tells Gutenberg which child element of the block to use.  For the title, we are telling Gutenberg that the HTML for this block contains an h2, and that the contents of the h2 are the title attribute.  This block also contains a div with a class of text, and the contents of that div are the text attribute.

We have used this code to tell Gutenberg how to parse the HTML that it finds in the content, and split it into the different attributes of our block.

Now that we know what the end product is, and now that we have told Gutenberg what our attributes are, we can build the block input in the WordPress editor:

    edit: function() {
        return [
            el( 'div',
                el( blocks.Editable, {
                    tagName: 'h2',
                    value: attributes.title,
                } ),
               el( blocks.Editable, {
                     tagName: 'div',
                     value: attributes.text,
               } )
            )
         ];
    },

This is very similar to the save function, for good reason: we want our block to look pretty much the same in both the editor and the front end of the site.  This code is telling Gutenberg that we want an editable h2, and if we already have a title attribute, then display that in the h2.  We want to have an editable div, and if we already have an attribute of text, then the text should be displayed in the div.

This should help you understand why Gutenberg code is structure the way it is, and the relationship between what happens in the editor and what happens in the post content.