Establishing a common, 'official' method to allow blocks to be extended by themes in specific contexts #18892
Description
There have already been some discussions about block extensibility (eg. #6023, #15450, #7931); the current issue is intended to be considered alongside those.
Blocks need to be agnostic to the theme they're used in; yet in some cases style or presentation options are only known to (or named by) the theme, and it is sometimes desirable to offer these as choices / controls within a block, possibly at a granular level affecting only specific elements.
Examples: a choice of (theme) font for a particular element within a block; a choice of (theme) colour for specific elements; a choice of photo filter for individual photos in a gallery.
The editor.blockEdit
filter already allows us to pass additional parameters into a block's edit()
function, so the underlying mechanism for this block-theme communication exists, but there doesn't seem to be a recommended / official method for it, so we've implemented a simple but versatile interface which we're proposing here for discussion.
We're already using this technique effectively in our own blocks / themes, but if consensus were reached on an official method, block developers could start using it consistently to expose extensible contexts to themes, and would only need to document the name and formats of their extensions (instead of citing long-winded hooks to be copy-pasted); while theme developers would have a consistent, tidy and stable API to register all their block extensions in one place.
Our proposed method consists of just a few additional lines of JS (currently this is an npm package which we import into theme JS enqueued for the block editor) which provide:
- a
registerBlockExtensions()
function for themes to register their extensions for specific blocks - the filter which takes care of passing the extended data into each respective block, which the blocks can then use to present a relevant UI
This file (fabrica-block-extensions/index.js
) looks like this:
const extensions = {};
const extendBlockEdit = (BlockEdit) => (
(props) => wp.element.createElement(BlockEdit, {...props, extensions: extensions[props.name]})
);
wp.hooks.addFilter('editor.BlockEdit', 'fabrica/extend-blocks', extendBlockEdit);
export default function registerBlockExtensions(contexts, data) {
if (typeof contexts === 'string') { contexts = [contexts]; }
if (typeof contexts !== 'object') { return false; }
contexts.forEach(context => extensions[context] = data);
}
Example usage:
Register extensions in the theme:
import registerBlockExtensions from 'fabrica-block-extensions';
registerBlockExtensions('fabrica/extensible-block', {
captionFonts: {
fontFranklinGothic: 'Franklin Gothic',
fontAveria: 'Averia',
fontCaslon: 'Caslon',
fontIawriter: 'iAwriter',
}
});
Receive and act upon extensions in block:
This example shows an additional radio control allowing font selection for a specific element within the block, but it could be a dropdown, toolbar or any other UI device – it's completely up to the block. The key is that the extensions
property sent to the edit function contains all the block's registered extensions.
registerBlockType('fabrica/extensible-block', {
(...)
edit: ({attributes, className, setAttributes, extensions}) => {
(...)
{ extensions && typeof(extensions.captionFonts) === 'object' && <RadioControl
selected={attributes.captionFont}
options={[{label: 'Default', value: ''}, ...Object.keys(extensions.captionFonts).map((key) => ({label: extensions.captionFonts[key], value: key}))]}
onChange={(value) => {setAttributes({captionFont: value})}}
/> }
}
Of course it will fall to block developers to accurately and transparently document the contexts in which they are receptive to extensions, as well as the data format they expect in each case; similarly it falls to them to escape safely if data passed in is malformed (hence the conditional checks wrapping the <RadioControl>
in the last example above). They should also escape gracefully if extension data is missing (which simply means a theme has opted not to pass in any extensions).
Anyone interested in testing this can install the npm package fabrica-block-extensions
.
Looking forward to reading thoughts and feedback.
Activity