Recently we had a Block Variation which needed to be switched from core/columns
to core/group
. Should have been easy enough, but the identity of the variation, its name and icon, would never show up. No matter what, the variation's isActive
method was not even being called.
It turns out that the various layouts available to the core/group
Group Block are all Block Variations. Each has it's own isActive
function, which simply checks the Block's layout.type
attribute and sometimes it's layout.orientation
.
The problem is that all of the isActive
functions for each of the Block's variations are run in order until one returns true, at which point WordPress stops checking. This is documented in the very last paragraph of the Block Editor Handbook's Variations page:
Note that specificity cannot be determined for a matching variation if its isActive property is a function rather than a string[]. In this case, the first matching variation will be determined to be the active variation. For this reason, it is generally recommended to use a string[] rather than a function for the isActive property.
A Block's isActive
functions run in Block Variation order and because registerBlockVariation
adds new Block Variations to the end of the list, the native Block Variations always run first. Since our variation is using the same layout attributes as the standard core/group
Block, those native isActive
functions return true before WordPress ever gets to our variation.
Ok, so that's why our isActive
method isn't running. Now, how to fix it?
My first thought was that there had to be some way to inject our Block Variation before the native variations, or to re-order the list of variations. This however, seems to be impossible.
There are limited filters available to the JavaScript side of the Block Editor, and the most promising, blocks.registerBlockType
only runs when the block is first initialized, at that moment, the variations I wanted to juggle don't exist yet.
Fine, let's get a bigger hammer.
Even though it's possible to get a concise list of variations from the browser console with wp.blocks.getBlockVariations('core/group')
, that didn't work from @wordpress/dom-ready or from the blocks.registerBlockType
filter. The list was either undefined or I got one of those hooks errors no one ever wants to deal with.
Because there was no easy way to loop over the list of variations and I didn't want to bake in a pre-defined list (those always break), this idea died pretty fast.
Had it worked, I would have looped the variations, stored an object-representation of the variation, unregistered each variation, registered our variation against a clean Block so it was the first variation available, then re-registered all the stored native variations back onto the Block after ours.
It seemed like a nice idea, but actually sounds kind of computationally expensive when written out like that.
Going back to the blocks.registerBlockType
filter, WordPress provides a way to modify Blocks upon instantiation. That means we have access to the built-in Block Variations prior to anything else being added.
While it might be possible to inject a raw variation manually from this filter, I've been working with WordPress long enough to know not to trust those kinds of shortcuts. Even if it worked initially, some future change would likely break everything because the variation wasn't registered with the standard APIs.
The solution turned out to be fairly simple, but felt like a stroke of genius: Instead of trying to inject our Variation into the list, why not just replace the isActive
functions on whatever Variations were available to filter in registerBlockType
? No matter what those functions actually do, they all have to take in the Block's attributes and return a boolean. I can work with that.
The advantage of using a decorator pattern is that our code changes very little -- nothing is really being replacing, instead a very thin layer is wrapping existing code. And because the condition we're adding defaults to true if our class doesn't exist, most everything keeps on working like it always has.
The new condition we're adding to isActive
looks like this:
const nameCheck = (blockAttributes) => !blockAttributes?.className;
That just checks for any namespace being set. Since none of the built-in variations use a namespace, we can skip them if any namespace is set on the block.
Each variation is looped, and if it's isActive
property is a function, then a decorated function is returned instead. (newSettings
is a copy of the filter's main payload which will be returned):
newSettings.variations = newSettings.variations.map((v) => {
const oldIsActive = v.isActive;
if (typeof oldIsActive === "function") {
v.isActive = (atts) => nameCheck(atts) && oldIsActive(atts);
}
return v;
});
First, the old isActive
function is copied into a variable. If that's a function, then we replace it with a wrapped/decorated function which calls the original function along with, and after, our new nameCheck
function.
The core/group
block doesn't have a namespace attribute, but we can add one from the blocks.registerBlockType
JavaScript filter. This doesn't set a value, only a definition, in this case a string, defaulting to empty:
newSettings.attributes = {
...newSettings.attributes,
namespace: { type: "string", default: "" },
};
Now that we have a known attribute, the block's isActive
method can simply be ['namespace']
string array, as recommended in the handbook.
Here's the complete filter:
addFilter("blocks.registerBlockType", "update-isActive", (settings, name) => {
const newSettings = { ...settings };
if (name === "core/group") {
newSettings.attributes = {
...newSettings.attributes,
namespace: { type: "string", default: "" },
};
const nameCheck = (blockAttributes) => !blockAttributes?.namespace;
newSettings.variations = newSettings.variations.map((v) => {
const oldIsActive = v.isActive;
if (typeof oldIsActive === "function") {
v.isActive = (atts) => nameCheck(atts) && oldIsActive(atts);
}
return v;
});
}
return newSettings;
});