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.

2 responses to “Adding a custom attribute to Gutenberg block

Kory

Thank you for the tutorial. I was looking to accomplish something similar by extending the core/pullquote block. My goal is to have a toggle in the block inspector that allows you to add an optional button beneath the normal <blockquote> and <cite> HTML elements. I have the basics working to get the toggle to show up for the core/pullquote block and to have the extra markup appear via the blocks.getSaveElement. What I'm wondering is how to make the extra markup appear within the Wordpress editor itself. If I edit the pullquote block on a page and toggle the newly added toggle button, no HTML changes are made in the editor. The only changes appear on the front-end. I am happy to send you a Gist for the code I'm working with if that's helpful. Thanks.

websevendev

Hello,

sorry for the late reply, this is probably the first non-spam comment on this site in 3 years so I don't usually pay attention to them.

You should be able to just render additional elements in editor.BlockEdit along with the BlockEdit and InspectorControls. Example: pastebin.com/5JkLaz6k

I'm not sure about getting the button inside the <figure> wrapper in the save filter though. Maybe if you make it a class and manipulate the BlockEdit element tree somehow (like here - Example 2: Modifying the React Elements tree outputted by render). Or alternatively you can look at the original block edit implementation and recreate it fully.

Leave a comment

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