creating your first gutenberg plugin – part II

March 28, 2019

In this follow up post we deal with transformations between blocks and take a look on deprecations.

In the meantime I’ve prepared a repository on github including a wordpress playground you can bootstrap with docker-compose.

github repo

To recap from the first article in this series we created a wordpress plugin that provides a simple, customized code block. Within this element we offer an extra caption and we manage the snippets’ programming language.

transformations

Whenever we switch between block types we internally apply a transformation. This is one type of transformation that enables one to change content without the need to recreate blocks. Of course there’s more. Within gutenberg there are several transformations available we are going to take a look upon.

block transformations

Imo the most important type of transformations is a so called block transformation. If blocks share a common purpose we want to provide a transformation to switch from one block functionality to the other and vice versa.

Let us continue by extending the custom jscouch/code block from the previous post. First of all we identify what should be transformable.

Of course we want to transform core/code. Additionaly there’s core/preformatted representing any monospaced text.

Remember, inside the editor if you press <enter> a new line appears – a core/paragraph element. Let’s pick it up as well as it represents mostly text and may increase editing comfort a bit, e.g. when the editor pastes any prepared content before changing the block type.

We start off by importing the createBlock function from @wordpress/blocks and extend our transformations inside the block attributes:

transforms from

import {createBlock, registerBlockType} from '@wordpress/blocks'

registerBlockType('jscouch/code', {
  transforms: {
    from: [
      {
        type: 'block',
        blocks: ['core/code', 'core/paragraph', 'core/preformatted'],
        transform: ({content}) => createBlock('jscouch/code', {content}),
      },
    ],
  },

The type property in our snippet declares a transformation targeting blocks. We define our whitelist on transformable block elements and forward the content attribute from the source block to our custom implementation target. We need to add an extra transformation from existing blocks backwards to core/core, so let’s extend the transform declaration.

transforms to

registerBlockType('jscouch/code', {
  transforms: {
    from: [...],
    to: [
      {
        type: 'block',
        blocks: ['core/code'],
        transform: ({content}) => createBlock('core/code', {content}),
      },
    ],
  },

Within the backwards transformation we tolerate to lose information as core/code displays less information. This time we just define core/code as a valid transformation target as these are the most similar blocks.

other transformations

Additionally our block can handle non-block content as well by registering a raw transformation. This import can be applied on unrecognized, externally changed content or if the block validation fails. core/code already defines a raw mutation. If you feel brave this is a perfect task to define a more explicit transformation matching our first block. Defining a raw transformation enables a perfect fallback if we ever ship a backwards incompatible version of our plugin.

Last but not least the Gutenberg editor handles code block generations from markdown syntax as well. This is a pattern based transformation that detects the backticks syntax you probably are already familiar with.

Try it yourself, input three backticks, press enter and your paragraph transforms into a code-block. We shouldn’t register the same transformation for our custom code block as both handlers would compete.

deprecations and mutations

Between plugin/theme releases our block definitions may change and lead to incompatibilities, block validation would fail and inside the editor existing content becomes unreachable. Once again the documentation is really excellent. There are already a lot of existing deprecations inside the block-libary codebase really worth a look. I’ll recap what I’ve found most important.

A block can have several deprecated versions. A deprecation will be tried if a parsed block appears to be invalid, or if there is a deprecation defined for which its isEligible property function returns true.

developer handbook – deprecated blocks

In a nutshell we declare a deprecations array and create an object within that kind of duplicates our block definition we want to modifiy. We extract attributes and supports properties before to reuse both on block and deprecation definition:

changing block markup

const attributes = {
    language: {
        type: 'string',
        default: 'javascript',
    },
    filePath: {
        type: 'string',
        source: 'text',
        selector: 'h4 span',
    },
    content: {
        type: 'string',
        source: 'text',
        selector: 'code',
    },
};

const supports = {
    html: false,
};

registerBlockType('jscouch/code', {{
    deprecated: [
        {
            supports,
            attributes,
            save({attributes, className}) {
                return (
                    <div className={className}>
                        <h4>
                            <span>{attributes.filePath}</span>
                        </h4>
                        <pre>
                            <code className={attributes.language}>{attributes.content}</code>
                        </pre>
                    </div>
                );
            }
        }
    ],
    save() {...}
});

Definitly easy to declare and a 100% backwards compatibile Now we are free to change the save method output to whatever we like as long as the attributes selectors aren’t changing.

Block validation would still fail and we get a warning printed in the dev console. Afterwards the validation flow will check the deprecated save method, passes and displays the editable content.

There’s no isEligible declaration as the editor is smart enough for not calling it anyways in this situation as we only declared one migration.

Imho the deprecation declaration feels too verbose and a little defensive. It could create a lot of code waste as usually each deprecation would stay there forever. There’s a recently opened discussion if block validation is too picky and a pure attribute based migration would reduce this overhead.

Leave a comment