Adding a custom attribute to Gutenberg block

The new WordPress block editor is great, but sometimes not very flexible as it needs time to mature. For example a reddit user wanted to add the rel="nofollow" attribute to the Button block that has a link. Currently your only option is to edit the block as HTML, and once you’ve done that, switching back to block mode will invalidate the block. It will still work, but you can no longer edit it. You can recover the block, but then the custom attribute would be removed. If you need to do this often it can become repetitive and not very efficient.

With some JavaScript knowledge it is easy to tap into the Gutenberg developer API and extend it’s functionality to suit your needs better.

Adding a rel attribute to Button block

First it’s needed to modify the original block’s attributes to add a place to store the value of your custom attribute. This can be done utilizing the blocks.registerBlockType filter.

ES6
wp.hooks.addFilter(
	'blocks.registerBlockType',
	'myPlugin/relAttribute',
	settings => {
		if(settings.name === 'core/button') {
			settings.attributes = {
				...settings.attributes,
				myPluginRel: {
					type: 'string',
					default: '',
				},
			};
		}
		return settings;
	}
);

We check if the block we are working on is the Button block (core/button) and if that is the case we add the custom attribute myPluginRel of type string. It is important to prefix your attribute names because a block may already use the attribute name rel or a third party plugin may add it instead.

Then we need a place to edit the value of the rel attribute. For that the editor.BlockEdit filter can be used to add some additional InspectorControls.

ES6
wp.hooks.addFilter(
	'editor.BlockEdit',
	'myPlugin/relInput',
	wp.compose.createHigherOrderComponent(
		BlockEdit => props => {
			if(props.name === 'core/button') {
				return (
					<wp.element.Fragment>
						<BlockEdit {...props} />
						{props.attributes.url && (
							<wp.blockEditor.InspectorControls>
								<wp.components.PanelBody title='My plugin'>
									<wp.components.TextControl
										label='Link rel'
										value={props.attributes.myPluginRel}
										onChange={nextRel => props.setAttributes({myPluginRel: nextRel})}
									/>
								</wp.components.PanelBody>
							</wp.blockEditor.InspectorControls>
						)}
					</wp.element.Fragment>
				);
			}
			return <BlockEdit {...props} />;
		},
		'withMyPluginRelInput'
	)
);

This code also takes care to check whether or not the button currently has a link – if there isn’t a link there is no reason to display the control because the rel attribute should only be applied to a link.

Gutenberg button block with rel attribute control

Finally, we need to apply the value of the myPluginRel attribute to the element that the block gets saved as.

ES6
wp.hooks.addFilter(
	'blocks.getSaveElement',
	'myPlugin/rel',
	(element, block, attributes) => {
		if(block.name === 'core/button') {
			if(attributes.myPluginRel && attributes.url) {
				return wp.element.cloneElement(
					element,
					{},
					wp.element.cloneElement(
						element.props.children,
						{rel: attributes.myPluginRel}
					)
				);
			}
		}
		return element;
	}
);

Again, we make sure the button has a link, or the url attribute, as well as the myPluginRel attribute. Then we clone the button wrapper element as well as it’s child element, which would be the <a> tag, and assign the value of myPluginRel to it as the rel prop.

Lastly, wrapping the 3 filters in wp.domReady() can help ensure that our code is loaded once everything is in order.

Custom
wp.domReady(() => {

	wp.hooks.addFilter(
		'blocks.registerBlockType',
		'myPlugin/relAttribute',
		settings => {
			if(settings.name === 'core/button') {
				settings.attributes = {
					...settings.attributes,
					myPluginRel: {
						type: 'string',
						default: '',
					},
				};
			}
			return settings;
		}
	);

	wp.hooks.addFilter(
		'editor.BlockEdit',
		'myPlugin/relInput',
		wp.compose.createHigherOrderComponent(
			BlockEdit => props => {
				if(props.name === 'core/button') {
					return (
						<wp.element.Fragment>
							<BlockEdit {...props} />
							{props.attributes.url && (
								<wp.blockEditor.InspectorControls>
									<wp.components.PanelBody title='My plugin'>
										<wp.components.TextControl
											label='Link rel'
											value={props.attributes.myPluginRel}
											onChange={nextRel => props.setAttributes({myPluginRel: nextRel})}
										/>
									</wp.components.PanelBody>
								</wp.blockEditor.InspectorControls>
							)}
						</wp.element.Fragment>
					);
				}
				return <BlockEdit {...props} />;
			},
			'withMyPluginRelInput'
		)
	);

	wp.hooks.addFilter(
		'blocks.getSaveElement',
		'myPlugin/rel',
		(element, block, attributes) => {
			if(block.name === 'core/button') {
				if(attributes.myPluginRel && attributes.url) {
					return wp.element.cloneElement(
						element,
						{},
						wp.element.cloneElement(
							element.props.children,
							{rel: attributes.myPluginRel}
						)
					);
				}
			}
			return element;
		}
	);
	
});

Next we need to compile this ES6 JavaScript to be compatible with browser environment. Babel Repl can be used to do it straight in your browser. Using the react and env presets should be enough.

The compiled code should look something like this:

JavaScript
"use strict";

function ownKeys(object, enumerableOnly) {
  var keys = Object.keys(object);
  if (Object.getOwnPropertySymbols) {
    keys.push.apply(keys, Object.getOwnPropertySymbols(object));
  }
  if (enumerableOnly)
    keys = keys.filter(function(sym) {
      return Object.getOwnPropertyDescriptor(object, sym).enumerable;
    });
  return keys;
}

function _objectSpread(target) {
  for (var i = 1; i < arguments.length; i++) {
    var source = arguments[i] != null ? arguments[i] : {};
    if (i % 2) {
      ownKeys(source, true).forEach(function(key) {
        _defineProperty(target, key, source[key]);
      });
    } else if (Object.getOwnPropertyDescriptors) {
      Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
    } else {
      ownKeys(source).forEach(function(key) {
        Object.defineProperty(
          target,
          key,
          Object.getOwnPropertyDescriptor(source, key)
        );
      });
    }
  }
  return target;
}

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}

wp.domReady(function() {
  wp.hooks.addFilter(
    "blocks.registerBlockType",
    "myPlugin/relAttribute",
    function(settings) {
      if (settings.name === "core/button") {
        settings.attributes = _objectSpread({}, settings.attributes, {
          myPluginRel: {
            type: "string",
            default: ""
          }
        });
      }

      return settings;
    }
  );
  wp.hooks.addFilter(
    "editor.BlockEdit",
    "myPlugin/relInput",
    wp.compose.createHigherOrderComponent(function(BlockEdit) {
      return function(props) {
        if (props.name === "core/button") {
          return React.createElement(
            wp.element.Fragment,
            null,
            React.createElement(BlockEdit, props),
            props.attributes.url &&
              React.createElement(
                wp.blockEditor.InspectorControls,
                null,
                React.createElement(
                  wp.components.PanelBody,
                  {
                    title: "My plugin"
                  },
                  React.createElement(wp.components.TextControl, {
                    label: "Link rel",
                    value: props.attributes.myPluginRel,
                    onChange: function onChange(nextRel) {
                      return props.setAttributes({
                        myPluginRel: nextRel
                      });
                    }
                  })
                )
              )
          );
        }

        return React.createElement(BlockEdit, props);
      };
    }, "withMyPluginRelInput")
  );
  wp.hooks.addFilter("blocks.getSaveElement", "myPlugin/rel", function(
    element,
    block,
    attributes
  ) {
    if (block.name === "core/button") {
      if (attributes.myPluginRel && attributes.url) {
        return wp.element.cloneElement(
          element,
          {},
          wp.element.cloneElement(element.props.children, {
            rel: attributes.myPluginRel
          })
        );
      }
    }

    return element;
  });
});

The compiled code can be saved to your child theme directory as rel.js. And then all you need to do is load the script on the block editor page by modifying your child theme’s functions.php:

PHP
<?php
add_action('enqueue_block_editor_assets', function() {
    wp_enqueue_script(
        'myplugin-rel',
        get_stylesheet_directory_uri() . '/rel.js',
        [
            'wp-blocks',
            'wp-components',
            'wp-compose',
            'wp-dom-ready',
            'wp-editor',
            'wp-element',
            'wp-hooks',
        ]
    );
});

The resulting Button HTML can now contain the rel attribute.

HTML
<div class="wp-block-button">
  <a class="wp-block-button__link" href="#" rel="nofollow">Click me</a>
</div>

By utilizing the Gutenberg Block Filters it’s possible to make your editor more convenient and effective to use. I hope this example gives a good overview on how to go about doing that in the most simplistic way.

Leave a comment

Your email address will not be published. Required fields are marked *