WordPress Gutenberg — Adapting Shortcodes to Work as Blocks

On November 19th, 2018 WordPress 5 is coming out. This major release completely changes the interface for writing pages and blog posts. The TinyMCE editor gets replaced by blocks — WordPress’ own page builder if you will. It is called the Gutenberg editor.

Gutenberg will change a lot of things. Third party page builders that have existed for years may no longer work, or may become unnecessary. Gutenberg is controversial and a major pivot for WordPress. You can learn more about Gutenberg and implications in this post from Delicious Brains.

Gutenberg is also written in React. This is both a blessing and a curse for developers. On one hand its a new paradigm to learn and adopt for plugin writers. On the other hand, React is eating front-end development. So its an opportunity to learn it.

In this post I’ll describe how I adapted the Amilia Store Plugin to support both shortcodes and blocks with minimal code changes. The full code for the plugin is available on the WordPress Plugin SVN repository.

The Shortcode

The Amilia Store Plugin adds extra buttons to the the TinyMCE toolbar. These buttons are used to insert shortcodes. On the published page, the shortcodes render HTML on the back-end in PHP. For example, the Amilia Store shortcode is used to embed a customers’ store in their website. In the editor, the TinyMCE button opens a popup to configure the embed. On the published page, we see the store embedded in an iframe.

Amilia Store Plugin Shortcode in TinyMCE Editor of WordPress

With Gutenberg, the blue Amilia buttons in TinyMCE will disappear. They will have to get replaced by blocks.

Conversion Strategy

To avoid having to rewrite a lot of code, I decided to adopt these strategies:

  1. Convert one of the shortcodes (the iframe embed) as a proof of concept.
  2. The iframe shortcode will become an Embed block.
  3. The shortcode popup will be converted to Inspector Controls for the block. Meaning the user will configure the iframe from the side panel. I will need to write new code for this.
  4. The iframe markup will still be generated in PHP by using a dynamic block. The shortcode and block will share that functionality. No need to rewrite that code. This is the biggest time saver.
  5. I will use ES5 and avoid ES6, webpack, babel and JSX. This reduces the learning curve and house keeping tasks I would need to spend time on to support transpiling, building and publishing. I can keep the number of files small and use the same workflow as before (commit to SVN).

The strategies are summarized in this diagram:

Sharing PHP code using a dynamic block

Bottom line I want my shortcode and Gutenberg block to co-exist and share code. I want to avoid having to duplicate code. At the same time I do not want the user experience to suffer. I therefore have to write new code for the block editor.

Plugin Code Modifications

Not much had to change for the existing plugin code. In amilia-store.php, I had to add an extra include to load block code.

...
// Shortcodes
include "shortcodes/amilia-store-iframe.php";
include "shortcodes/amilia-store-table.php";
include "shortcodes/amilia-store-calendar.php";
include "shortcodes/amilia-store-standings.php";
// Gutenberg blocks
include "blocks/amilia-store-iframe.php";

The Block Boilerplate

Registering a new block requires some boilerplate. Fortunately, WP-CLI can be used to generate Gutenberg blocks for you in your existing plugin. I used it and it saved me a lot of time. After some cleanup, this is my block registration PHP file amilia-store-iframe.php:

<?php
function amilia_store_iframe_block_init() {
if ( ! function_exists( 'register_block_type' ) ) {
return;
}
$dir = dirname( __FILE__ );
$index_js = 'amilia-store-iframe.js';
wp_register_script(
'amilia-store-iframe-block-editor',
plugins_url( $index_js, __FILE__ ),
array(
'wp-blocks',
'wp-i18n',
'wp-element',
),
filemtime( "$dir/$index_js" )
);
register_block_type( 'amilia-store/amilia-store-iframe', array(
'editor_script' => 'amilia-store-iframe-block-editor',
'attributes' => array(
'url' => array('type' => 'string'),
'color' => array('type' => 'string')
),
'render_callback' => 'amilia_store_iframe_shortcode_handler'
) );
}
add_action( 'init', 'amilia_store_iframe_block_init' );

The register_block_type function call was modified to indicate attributes to be persisted, along with the render_callback function. It is the back-end render function, the same used for the shortcode. When called, it will be passed the attributes. It is fortunate that block and shortcode rendering functions for dynamic blocks have the same signature.

I also had to remove the boilerplate that registered site CSS and JS since the shortcode handler already takes care of that.

The Block Editor UI

This is what the editor looks like:

WordPress Gutenberg — Amilia Store Block

The user experience is pretty slick. We add the block and can then quickly modify its attributes via the side panel. We then hit preview to see what it looks like.

Note: You can render the block server side in the editor by using the ServerSideRender component.

Block JavaScript

The block’s code is the one using React. In my case it is written in traditional ES5 to avoid having to install webpack and run Babel to transpile. The ES5 code can be interpreted as is by any modern browser. The full code is presented at the end of this section for you to consult.

Dependency Injection

A block is injected with the wp variable which contains every required dependency. The first lines of a block are typically fetching those dependencies and making them available in scoped variables.

(function(wp) {
var registerBlockType = wp.blocks.registerBlockType;
var InspectorControls = wp.editor.InspectorControls;
var PanelBody = wp.components.PanelBody;
var TextControl = wp.components.TextControl;
var ColorPicker = wp.components.ColorPicker;
var ColorPalette = wp.components.ColorPalette;
var SelectControl = wp.components.SelectControl;
var Dashicon = wp.components.Dashicon;
var el = wp.element.createElement;
var withState = wp.compose.withState;
var __ = wp.i18n.__;
...

In addition, block components also get their dependencies injected through the famous props argument. You will find attributes in there, as well as a helper method setAttributes to persist them.

function AmiliaControl(props) {
var attributes = props.attributes;
var setAttributes = props.setAttributes;
...

Block Registration

A Gutenberg block needs to be registered using the registerBlockType function. In that function you pass attributes, the edit render method and the save render method (what renders on the published page). Since I am using a dynamic block that renders site markup using PHP, I return null in my save method.

registerBlockType('amilia-store/amilia-store-iframe', {
title: __('Amilia Store'),
category: 'embed',
icon: {
foreground: '#46aaf8',
src: 'store'
},
attributes: {
url: {
type: 'string',
default: null
},
color: {
type: 'string',
default: '#46aaf8'
}
},
edit: withState({status: ''})(AmiliaControl),
save: function(props) {
return null;
}
});

Edit Components

The edit method is where you compose the UI with React components. You simply return a list of components to render on screen. In ES5, you use the helper function wp.element.createElement conveniently shortened to el to instantiate a component. WordPress comes with a full library of components. In my case I used TextControl and a ColorPalette components to configure my block. I wrapped these in an InspectorControls component to make them appear in the side bar. Other components get rendered inline in the block. It is kind of strange but that’s how it works.

WordPress Gutenberg Editor — Amilia Store Block Components

React components are pretty easy to use. You pass them display options and an onChange callback to save user changes. Very similar to traditional jQuery plugins. Myself I had a lot of fun trying different components. To capture the color attribute for example, I played with a SelectControl, then a ColorPicker but landed on the ColorPalette. This is where Gutenberg shines — offering a large number of components for block developers to use.

Attributes

Attributes are the bits and pieces that get persisted for the block. In the editor you write components that will show and fetch them from the user. On published pages, WordPress will pass those to the PHP shortcode handler.

State

WordPress provides a handy withState helper function. State in React is information the component needs to handle and render for business logic — but which does not get persisted into attributes. In my plugin for example, I perform an AJAX call to validate the URL passed by the user. The output of that validation is stored in a state property called status. Checkout the Popover component for another example.

Full Code Reference

File amilia-store-iframe.js contains this:

(function(wp) {
var registerBlockType = wp.blocks.registerBlockType;
var InspectorControls = wp.editor.InspectorControls;
var PanelBody = wp.components.PanelBody;
var TextControl = wp.components.TextControl;
var ColorPicker = wp.components.ColorPicker;
var ColorPalette = wp.components.ColorPalette;
var SelectControl = wp.components.SelectControl;
var Dashicon = wp.components.Dashicon;
var el = wp.element.createElement;
var withState = wp.compose.withState;
var __ = wp.i18n.__;
function AmiliaControl(props) {
var attributes = props.attributes;
var setAttributes = props.setAttributes;
var setState = props.setState;
var status = props.status;
var url = attributes.url === null ? window.Amilia.storeUrl : attributes.url;
function onValidateUrl(result) {
setState({status: result.message});
}
if (status === '') setState({status: Amilia.validateStoreUrlString(url, onValidateUrl).message});
var inspectorControl = el(InspectorControls, {}, 
el('h4', {}, el('span', {}, Amilia.lang('iframe-title'))),
el(TextControl, {
label: Amilia.lang('url-label'),
value: url,
onChange: function(value) {
setAttributes({url: value});
setState({status: Amilia.validateStoreUrlString(value, onValidateUrl).message});
}
}),
el('p', {className: 'input-helper'}, status),
el('label', {}, Amilia.lang('color')),
el(ColorPalette, {
color: attributes.color,
colors: Object.keys(Amilia.COLORS).map(function(k) {
return {name: Amilia.lang(k), color: Amilia.COLORS[k]};
}),
onChange: function(value) {
setAttributes({color: value});
}
}),
el(PanelBody, {title: Amilia.lang('help'), initialOpen: false},
el('p', {}, Amilia.lang('instructions-p1')),
el('p', {}, Amilia.lang('instructions-p2')),
el('p', {}, Amilia.lang('instructions-p3')),
el('p', {}, Amilia.lang('instructions-p4'))
)
);
return el('div', {
className: 'amilia-store-block',
style: {
backgroundColor: attributes.color,
color: Amilia.invertColor(attributes.color)
}
},
el('img', {src: Amilia.pluginUrl + 'images/amilia-a.svg', className: 'logo'}),
el('p', {className: 'strong'}, Amilia.lang('amilia-store')),
el('p', {className: 'italic'}, Amilia.lang('block-info')),
inspectorControl
);
}
registerBlockType('amilia-store/amilia-store-iframe', {
title: __('Amilia Store'),
category: 'embed',
icon: {
foreground: '#46aaf8',
src: 'store'
},
attributes: {
url: {
type: 'string',
default: null
},
color: {
type: 'string',
default: '#46aaf8'
}
},
edit: withState({status: ''})(AmiliaControl),
save: function(props) {
return null;
}
});
})(window.wp);

Wrapping Up

As daunting as Gutenberg blocks and React was at first, it wasn’t that bad in the end. Fortunately, blocks can be written in ES5 and work out of the box in any modern browser. Blocks can also share the PHP rendering functions used by shortcodes thanks to dynamic blocks and the ServerSideRender component. Those things helped me to reduce the time to adapt my existing shortcodes to blocks.

In the future though, I do plan to migrate all block code to JavaScript and, more specifically, to ES6 (and JSX). This will necessitate webpack and Babel to transpile the source code. As well, source code will most likely be hosted on GitHub. A webpack task will be necessary to build distribution files to be committed to the SVN repository. A lot of work required here. However once in place, plugin enhancements will become accessible to my fellow React developer colleagues. Writing PHP will no longer be necessary. They will be happy.

Thanks to Jérémie Bryon, Alexandre-Ho Latreille and Daniel Tousignant for reading a draft of this post.