Blocks in WordPress are great. Drop some into the page, arrange them how you like, and you’ve got a pretty sweet landing page with little effort. But what if the default blocks in WordPress need a little tweaking? Like, what if we could remove the alignment options in the Cover block settings? Or how about control sizing for the Button block?

There are plenty of options when it comes to extending the functionality of core blocks in WordPress. We can add a custom CSS class to a block in the editor, add a custom style, or create a block variation. But even those might not be enough to get what you need, and you find yourself needing to filter the core block to add or remove features, or building an entirely new block from scratch.

I’ll show you how to extend core blocks with filters and also touch on when it’s best to build a custom block instead of extending a core one.

A quick note on these examples

Before we dive in, I’d like to note that code snippets in this article are taken out of context on purpose to focus on filters rather than build tools and file structure. If I included the full code for the filters, the article would be hard to follow. With that said, I understand that it’s not obvious for someone who is just starting out where to put the snippets or how to run build scripts and make the whole thing work.

To make things easier for you, I made a WordPress plugin with examples from this article available on my GitHub. Feel free to download it and explore the file structure, dependencies and build scripts. There is a README that will help you get started.

Block filters in an nutshell

The concept of filters is not new to WordPress. Most of us are familiar with the add_filter() function in PHP. It allows developers to modify various types of data using hooks.

A simple example of a PHP filter could look something like this:

function filter_post_title( $title ){
  return '<strong>' . $title . '</strong>';
};

add_filter( 'the_title',  'filter_post_title' );

In this snippet, we create a function that receives a string representing a post title, then wrap it in a <strong> tag and return a modified title. We then use add_filter() to tell WordPress to use that function on a post title.

JavaScript filters work in a similar way. There is a JavaScript function called addFilter() that lives in the wp.hooks package and works almost like its PHP sibling. In its simplest form, a JavaScript filter looks something like this:

function filterSomething(something) {
  // Code for modifying something goes here.
  return something;
}

wp.hooks.addFilter( 'hookName', 'namespace', filterSomething );

Looks pretty similar, right? One notable difference is addFilter() has a namespace as a second argument. As per the WordPress Handbook, “Namespace uniquely identifies a callback in the the form vendor/plugin/function.” However, examples in the handbook follow different patterns: plugin/what-filter-does or plugin/component-name/what-filter-does. I usually follow the latter because it keeps the handles unique throughout the project.

What makes JavaScript filters challenging to understand and use is the different nature of what they can filter. Some filter strings, some filter JavaScript objects, and others filter React components and require understanding the concept of Higher Order Components.

On top of that, you’ll most likely need to use JSX which means you can’t just drop the code into your theme or plugin and expect it to work. You need to transpile it to vanilla JavaScript that browsers understand. All that can be intimidating at the beginning, especially if you are coming from a PHP background and have limited knowledge of ES6, JSX, and React.

But fear not! We have two examples that cover the basics of block filters to help you grasp the idea and feel comfortable working with JavaScript filters in WordPress. As a reminder, if writing this code for the Block Editor is new to you, explore the plugin with examples from this article.

Without any further ado, let’s take a look at the first example.

Removing the Cover block’s alignment options

We’re going to filter the core Cover block and remove the Left, Center, Right, and Wide alignment options from its block settings. This may be useful on projects where the Cover block is only used as a page hero, or a banner of some sort and does not need to be left- or right-aligned.

We’ll use the blocks.registerBlockType filter. It receives the settings of the block and its name and must return a filtered settings object. Filtering settings allows us to update the supports object that contains the array of available alignments. Let’s do it step-by-step.

We’ll start by adding the filter that just logs the settings and the name of the block to the console, to see what we are working with:

const { addFilter } = wp.hooks;

function filterCoverBlockAlignments(settings, name) {
  console.log({ settings, name });
  return settings;
}

addFilter(
  'blocks.registerBlockType',
  'intro-to-filters/cover-block/alignment-settings',
  filterCoverBlockAlignments,
);

Let’s break it down. The first line is a basic destructuring of the wp.hooks object. It allows us to write addFilter() in the rest of the file, instead of wp.hooks.addFilter(). This may seem redundant in this case, but it is useful when using multiple filters in the same file (as we’ll get to in the next example).

Next, we defined the filterCoverBlockAlignments() function that does the filtering. For now, it only logs the settings object and the name of the block to the console and returns the settings as is.

All filter functions receive data, and must return filtered data. Otherwise, the editor will break.

And, lastly, we initiated the filter with addFilter() function. We provided it with the name of the hook we are going to use, the filter namespace, and a function that does the filtering.

If we’ve done everything right, we should see a lot of messages in the console. But note that not all of them refer to the Cover block.

This is correct because the filter is applied to all blocks rather than the specific one we want. To fix that, we need to make sure that we apply the filter only to the core/cover block:

function filterCoverBlockAlignments(settings, name) {
  if (name === 'core/cover') {
    console.log({ settings, name });
  }
  return settings;
}

With that in place, we should see something like this now in the console:

Don’t worry if you see more log statements than Cover blocks on the page. I have yet to figure out why that’s the case. If you happen to know why, please share in the comments!

And here comes the fun part: the actual filtering. If you have built blocks from scratch before, then you know that alignment options are defined with Supports API. Let me quickly remind you how it works — we can either set it to true to allow all alignments, like this:

supports: {
  align: true
}

…or provide an array of alignments to support. The snippet below does the same thing, as the one above:

supports: {
  align: [ 'left', 'right', 'center', 'wide', 'full' ]
}

Now let’s take a closer look at the settings object from one of the console messages we have and see what we are dealing with:

All we need to do is replace align: true with align: ['full'] inside the supports property. Here’s how we can do it:

function filterCoverBlockAlignments(settings, name) {
  if (name === 'core/cover') {
    return assign({}, settings, {
      supports: merge(settings.supports, {
        align: ['full'],
      }),
    });
  }
  return settings;
}

I’d like to pause here to draw your attention to the assign and merge lodash methods. We use those to create and return a brand new object and make sure that the original settings object remains intact. The filter will still work if we do something like this:

/* 👎 WRONG APPROACH! DO NOT COPY & PASTE! */
settings.supports.align = ['full'];
return settings;

…but that is an object mutation, which is considered a bad practice and should be avoided unless you know what you are doing. Zell Liew discusses why mutation can be scary over at A List Apart.

Going back to our example, there should now only be one alignment option in the block toolbar:

I removed the “center” alignment option because the alignment toolbar allows you to toggle the alignment “on” and “off.” This means that Cover blocks now have default and “Full width” states.

And here’s the full snippet:

const { addFilter } = wp.hooks;
const { assign, merge } = lodash;

function filterCoverBlockAlignments(settings, name) {
  if (name === 'core/cover') {
    return assign({}, settings, {
      supports: merge(settings.supports, {
        align: ['full'],
      }),
    });
}
  return settings;
}

addFilter(
  'blocks.registerBlockType',
  'intro-to-filters/cover-block/alignment-settings',
  filterCoverBlockAlignments,
);

This wasn’t hard at all, right? You are now equipped with a basic understanding of how filters work with blocks. Let’s level it up and take a look at a slightly more advanced example.

Adding a size control to the Button block

Now let’s add a size control to the core Button block. It will be a bit more advanced as we will need to make a few filters work together. The plan is to add a control that will allow the user to choose from three sizes for a button: Small, Regular, and Large.

The goal is to get that new “Size settings” section up and running.

It may seem complicated, but once we break it down, you’ll see that it’s actually pretty straightforward.

1. Add a size attribute to the Button block

First thing we need to do is add an additional attribute that stores the size of the button. We’ll use the already familiar blocks.registerBlockType filter from the previous example:

/**
 * Add Size attribute to Button block
 *
 * @param  {Object} settings Original block settings
 * @param  {string} name     Block name
 * @return {Object}          Filtered block settings
 */
function addAttributes(settings, name) {
  if (name === 'core/button') {
    return assign({}, settings, {
      attributes: merge(settings.attributes, {
        size: {
          type: 'string',
          default: '',
        },
      }),
    });
  }
  return settings;
}

addFilter(
  'blocks.registerBlockType',
  'intro-to-filters/button-block/add-attributes',
  addAttributes,
);

The difference between what we’re doing here versus what we did earlier is that we’re filtering attributes rather than the supports object. This snippet alone doesn’t do much and you won’t notice any difference in the editor, but having an attribute for the size is essential for the whole thing to work.

2. Add the size control to the Button block

We’re working with a new filter, editor.BlockEdit. It allows us to modify the Inspector Controls panel (i.e. the settings panel on the right of the Block editor).

/**
 * Add Size control to Button block
 */
const addInspectorControl = createHigherOrderComponent((BlockEdit) => {
  return (props) => {
    const {
      attributes: { size },
      setAttributes,
      name,
    } = props;
    if (name !== 'core/button') {
      return <BlockEdit {...props} />;
    }
    return (
      <Fragment>
        <BlockEdit {...props} />
        <InspectorControls>
          <PanelBody
            title={__('Size settings', 'intro-to-filters')}
            initialOpen={false}
          >
            <SelectControl
              label={__('Size', 'intro-to-filters')}
              value={size}
              options={[
                {
                  label: __('Regular', 'intro-to-filters'),
                  value: 'regular',
                },
                {
                  label: __('Small', 'intro-to-filters'),
                  value: 'small'
                },
                {
                  label: __('Large', 'intro-to-filters'),
                  value: 'large'
                },
              ]}
              onChange={(value) => {
                setAttributes({ size: value });
              }}
            />
          </PanelBody>
      </InspectorControls>
      </Fragment>
    );
  };
}, 'withInspectorControl');

addFilter(
  'editor.BlockEdit',
  'intro-to-filters/button-block/add-inspector-controls',
  addInspectorControl,
);

This may look like a lot, but we’ll break it down and see how straightforward it actually is.

The first thing you may have noticed is the createHigherOrderComponent construct. Unlike other filters in this example, editor.BlockEdit receives a component and must return a component. That’s why we need to use a Higher Order Component pattern derived from React.

In its purest form, the filter for adding controls looks something like this:

const addInspectorControl = createHigherOrderComponent((BlockEdit) => {
  return (props) => {
    // Logic happens here.
    return <BlockEdit {...props} />;
  };
}, 'withInspectorControl');

This will do nothing but allow you to inspect the <BlockEdit /> component and its props in the console. Hopefully the construct itself makes sense now, and we can keep breaking down the filter.

The next part is destructuring the props:

const {
  attributes: { size },
  setAttributes,
  name,
} = props;

This is done so we can use name, setAttributes, and size in the scope of the filter, where:

  • size is the attribute of the block that we’ve added in step 1.
  • setAttributes is a function that lets us update the block’s attribute values.
  • name is a name of the block. which is core/button in our case.

Next, we avoid inadvertantly adding controls to other blocks:

if (name !== 'core/button') {
  return <BlockEdit {...props} />;
}

And if we are dealing with a Button block, we wrap the settings panel in a <Fragment /> (a component that renders its children without a wrapping element) and add an additional control for picking the button size:

return (
  <Fragment>
    <BlockEdit {...props} />
    {/* Additional controls go here */}
  </Fragment>
);

Finally, additional controls are created like this:

<InspectorControls>
  <PanelBody title={__('Size settings', 'intro-to-filters')} initialOpen={false}>
    <SelectControl
      label={__('Size', 'intro-to-filters')}
      value={size}
      options={[
        { label: __('Regular', 'intro-to-filters'), value: 'regular' },
        { label: __('Small', 'intro-to-filters'), value: 'small' },
        { label: __('Large', 'intro-to-filters'), value: 'large' },
      ]}
      onChange={(value) => {
        setAttributes({ size: value });
      }}
    />
  </PanelBody>
</InspectorControls>

Again, if you have built blocks before, you may already be familiar with this part. If not, I encourage you to study the library of components that WordPress comes with.

At this point we should see an additional section in the inspector controls for each Button block:

We are also able to save the size, but that won’t reflect in the editor or on the front end. Let’s fix that.

3. Add a size class to the block in the editor

As the title suggests, the plan for this step is to add a CSS class to the Button block so that the selected size is reflected in the editor itself.

We’ll use the editor.BlockListBlock filter. It is similar to editor.BlockEdit in the sense that it receives the component and must return the component; but instead of filtering the block inspector panel, if filters the block component that is displayed in the editor.

import classnames from 'classnames';
const { addFilter } = wp.hooks;
const { createHigherOrderComponent } = wp.compose;

/**
 * Add size class to the block in the editor
 */
const addSizeClass = createHigherOrderComponent((BlockListBlock) => {
  return (props) => {
    const {
      attributes: { size },
      className,
      name,
    } = props;

    if (name !== 'core/button') {
      return <BlockListBlock {...props} />;
    }

    return (
      <BlockListBlock
        {...props}
        className={classnames(className, size ? `has-size-${size}` : '')}
      />
    );
  };
}, 'withClientIdClassName');

addFilter(
   'editor.BlockListBlock',
   'intro-to-filters/button-block/add-editor-class',
   addSizeClass
);

You may have noticed a similar structure already:

  1. We extract the size, className, and name variables from props.
  2. Next, we check if we are working with core/button block, and return an unmodified <BlockListBlock> if we aren’t.
  3. Then we add a class to a block based on selected button size.

I’d like to pause on this line as it may look confusing from the first glance:

className={classnames(className, size ? `has-size-${size}` : '')}

I’m using the classnames utility here, and it’s not a requirement — I just find using it a bit cleaner than doing manual concatenations. It prevents me from worrying about forgetting to add a space in front of a class, or dealing with double spaces.

4. Add the size class to the block on the front end

All we have done up to this point is related to the Block Editor view, which is sort of like a preview of what we might expect on the front end. If we change the button size, save the post and check the button markup on the front end, notice that button class is not being applied to the block.

To fix this, we need to make sure we are actually saving the changes and adding the class to the block on the front end. We do it with blocks.getSaveContent.extraProps filter, which hooks into the block’s save() function and allows us to modify the saved properties. This filter receives block props, the type of the block, and block attributes, and must return modified block props.

import classnames from 'classnames';
const { assign } = lodash;
const { addFilter } = wp.hooks;

/**
 * Add size class to the block on the front end
 *
 * @param  {Object} props      Additional props applied to save element.
 * @param  {Object} block      Block type.
 * @param  {Object} attributes Current block attributes.
 * @return {Object}            Filtered props applied to save element.
 */
function addSizeClassFrontEnd(props, block, attributes) {
  if (block.name !== 'core/button') {
    return props;
  }

  const { className } = props;
  const { size } = attributes;

  return assign({}, props, {
    className: classnames(className, size ? `has-size-${size}` : ''),
  });
}

addFilter(
  'blocks.getSaveContent.extraProps',
  'intro-to-filters/button-block/add-front-end-class',
  addSizeClassFrontEnd,
);

In the snippet above we do three things:

  1. Check if we are working with a core/button block and do a quick return if we are not.
  2. Extract the className and size variables from props and attributes objects respectively.
  3. Create a new props object with an updated className property that includes a size class if necessary.

Here’s what we should expect to see in the markup, complete with our size class:

<div class="wp-block-button has-size-large">
  <a class="wp-block-button__link" href="https://css-tricks.com/#">Click Me</a>
</div>

5. Add CSS for the custom button sizes

One more little thing before we’re done! The idea is to make sure that large and small buttons have corresponding CSS styles.

Here are the styles I came up with:

.wp-block-button.has-size-large .wp-block-button__link {
  padding: 1.5rem 3rem;
}
.wp-block-button.has-size-small .wp-block-button__link {
  padding: 0.25rem 1rem;
}

If you are building a custom theme, you can include these front-end styles in the theme’s stylesheet. I created a plugin for the default Twenty Twenty One theme, so, in my case, I had to create a separate stylesheet and include it using wp_enqueue_style(). You could just as easily work directly in functions.php if that’s where you manage functions.

function frontend_assets() {
  wp_enqueue_style(
    'intro-to-block-filters-frontend-style',
    plugin_dir_url( __FILE__ ) . 'assets/frontend.css',
    [],
    '0.1.0'
  );
}
add_action( 'wp_enqueue_scripts', 'frontend_assets' );

Similar to the front end, we need to make sure that buttons are properly styled in the editor. We can include the same styles using the enqueue_block_editor_assets action:

function editor_assets() {
  wp_enqueue_style(
    'intro-to-block-filters-editor-style',
    plugin_dir_url( __FILE__ ) . 'assets/editor.css',
    [],
    '0.1.0'
  );
}
add_action( 'enqueue_block_editor_assets', 'editor_assets' );

We should now should have styles for large and small buttons on the front end and in the editor!

As I mentioned earlier, these examples are available in as a WordPress plugin I created just for this article. So, if you want to see how all these pieces work together, download it over at GitHub and hack away. And if something isn’t clear, feel free to ask in the comments.

Use filters or create a new block?

This is a tricky question to answer without knowing the context. But there’s one tip I can offer.

Have you ever seen an error like this?

It usually occurs when the markup of the block on the page is different from the markup that is generated by the block’s save() function. What I’m getting at is it’s very easy to trigger this error when messing around with the markup of a block with filters.

So, if you need to significantly change the markup of a block beyond adding a class, I would consider writing a custom block instead of filtering an existing one. That is, unless you are fine with keeping the markup consistent for the editor and only changing the front-end markup. In that case, you can use PHP filter.

Speaking of which…

Bonus tip: render_block()

This article would not be complete without mentioning the render_block hook. It filters block markup before it’s rendered. It comes in handy when you need to update the markup of the block beyond adding a new class.

The big upside of this approach is that it won’t cause any validation errors in the editor. That said, the downside is that it only works on the front end. If I were to rewrite the button size example using this approach, I would first need to remove the code we wrote in the fourth step, and add this:

/**
 * Add button size class.
 *
 * @param  string $block_content Block content to be rendered.
 * @param  array  $block         Block attributes.
 * @return string
 */
function add_button_size_class( $block_content = '', $block = [] ) {
  if ( isset( $block['blockName'] ) && 'core/button' === $block['blockName'] ) {
    $defaults = ['size' => 'regular'];
    $args = wp_parse_args( $block['attrs'], $defaults );

    $html = str_replace(
      '<div class="wp-block-button',
      '<div class="wp-block-button has-size-' . esc_attr( $args['size']) . ' ',
      $block_content
    );

    return $html;
}
  return $block_content;
}

add_filter( 'render_block', 'add_button_size_class', 10, 2 );

This isn’t the cleanest approach because we are injecting a CSS class using str_replace() — but that’s sometimes the only option. A classic example might be working with a third-party block where we need to add a <div> with a class around it for styling.

Wrapping up

WordPress block filters are powerful. I like how it allows you to disable a lot of unused block options, like we did with the Cover block in the first example. This can reduce the amount of CSS you need to write which, in turn, means a leaner stylesheet and less maintenance — and less cognitive overhead for anyone using the block settings.

But as I mentioned before, using block filters for heavy modifications can become tricky because you need to keep block validation in mind.

That said, I usually reach for block filters if I need to:

  • disable certain block features,
  • add an option to a block and can’t/don’t want to do it with custom style (and that option must not modify the markup of the block beyond adding/removing a custom class), or
  • modify the markup only on the front end (using a PHP filter).

I also usually end up writing custom blocks when core blocks require heavy markup adjustments both on the front end and in the editor.

If you have worked with block filters and have other thoughts, questions, or comments, let me know!

Resources



Source link

Write A Comment