Editing a Gutenberg Core Block

For a recent site, I needed a <span> tag to wrap the content of a button link. This could also have been a custom block type, but I was able to alter the core/button block instead.

Add the filter

The blocks.getSaveElement filter allows us to alter the result of the block’s save function. In other words, what’s displayed on the front end.

import { addFilter } from '@wordpress/hooks';
addFilter(
  'blocks.getSaveElement',
  'my-wp-theme/button',
  updateBlockMarkup
);

Add the function

The filter above calls the function updateBlockMarkup to alter the save element. That function takes the element and blockType as parameters.

const updateBlockMarkup = (element, blockType) => {
  if (!element || blockType.name !== 'core/button') return element;
};  

Since we only want to alter buttons, we check the block name and return the unaltered element if we have a different block type.

Create the span

Right now, the text displayed for the button is the value prop on the element’s children prop. (It looks like element.props.children.props.value). We want to retrieve that value and then create a new span element with that value. We’ll use @wordpress/element’s createElement, which is an abstraction layer on top of React’s createElement.

import { createElement } from '@wordpress/element';
...
const { children } = element.props;
if (!children) return element;
const { value, ...childProps } = children.props;
const spanElem = createElement(
  'span',
  {
    className: 'button-inner',
  },
  value
);

Recreate the children

You’ll notice ...childProps in the code above. We need any props on the button, other than value, to recreate the a tag around the span. Cloning the element doesn’t work because it copies over the value as well, and the value is rendered instead of the children.

const anchorElem = createElement('a', childProps, spanElem);

Note that this assumes the element is always a link. If we needed to accommodate buttons that were created as span or button, we’d want to revisit this approach.

Clone the element

Finally, we return a clone of the original element (using cloneElement, which is likewise an abstraction on top of the React API), using the new anchorElem as the children instead of the original children.

return cloneElement(element, {}, anchorElem);

So altogether, the updateBlockMarkup function looks like this:

import { createElement, cloneElement } from '@wordpress/element';
const updateBlockMarkup = (element, blockType) => {
  if (!element || blockType.name !== 'core/button') return element;
  const { children } = element.props;
  if (!children) return element;
  const { value, ...childProps } = children.props;
  const spanElem = createElement(
    'span',
    {
      className: 'button-inner',
    },
    value
  );
  const anchorElem = createElement('a', childProps, spanElem);
  return cloneElement(element, {}, anchorElem);
}; 

Update the attribute

There’s one problem left. The core/button block gets the button text from the contents of the a tag. Since those now include the span, the markup ends up part of the value, and the saved element doesn’t match what WordPress expects. To fix that, we can alter the block metadata to use the span tag as the selector instead. This can be done using the block_type_metadata filter in functions.php.

function my_wp_theme_block_metadata_registration( $metadata ) {
  if ($metadata['name'] === 'core/button') {
    $metadata['attributes']['text']['selector'] = 'span';
  }
  return $metadata;
}
add_filter( 'block_type_metadata', 'my_wp_theme_block_metadata_registration' );
References