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.
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.
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
.
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.
Finally, we need to apply the value of the myPluginRel
attribute to the element that the block gets saved as.
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.
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:
"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
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.
<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.