Skip to content

Instantly share code, notes, and snippets.

@joemaller
Last active June 20, 2024 03:06
Show Gist options
  • Save joemaller/fd465d5716bea21532f039995edb1b7d to your computer and use it in GitHub Desktop.
Save joemaller/fd465d5716bea21532f039995edb1b7d to your computer and use it in GitHub Desktop.
A solution for determining isActive in Wordpress (Gutenberg) core/group Block Variations.

Creating Block Varitions from core/groups

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.

The problem

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?

Re-ordering variations?

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.

Unregister built-ins?

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.

An out of the box solution

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.

Enabling the namespace attribute

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: "" },
};

A simpler isActive

Now that we have a known attribute, the block's isActive method can simply be ['namespace'] string array, as recommended in the handbook.

Final code

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;
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment