Reusable Components in WordPress Gutenberg

In a recent Gutenberg project I had situations where we needed to build many blocks that shared very similar smaller components. How often have you had to create a button that links to another page? This begs for a reusable component.

With Gutenberg it gets a little tiring because each time you have a button like this, not only do you need to make the text on the button editable but you also need to edit the link. And often times you need to change whether the button is primary or secondary. On top of all this, you need to create the view() version of this. You need to attach attributes and make sure they update correctly.

It’s a lot of work. So I started to fall into a pattern of building components that seriously cut down my development time!

Creating a reusable button with a link

Let’s begin with a blank Gutenberg block. If you need a quick environment to set up in, check out my Guty blocks 2.

So I’m going to name my block button-example. We’re going to assume this is some block that uses the button component, but we’re not going to spend much time on the block. We’re here to make a reusable component!

So this is our initial block:

import './button-example.view.scss';
import './button-example.editor.scss';

const {
    registerBlockType,
} = wp.blocks;

const { 
    InspectorControls,
    RichText
} = wp.editor;

registerBlockType('test/button-example', {
    title: 'Button Example',
    icon: 'smiley',
    category: 'common',

    supports: {
        align: true,
    },

    attributes: {
        align: true,
    },

    edit(props) {
        const { className, setAttributes } = props;
        // const {  } = props.attributes;

        return [
            <InspectorControls>
                <div style={{padding: '1em 0'}}>
                    Options
                </div>
            </InspectorControls>,
            <div className={className}>
                Button Example
            </div>,
        ];
    },

    save(props) {
        // const className = getBlockDefaultClassName('test/button-example'); // For use with say, BEM
        // const {  } = props.attributes;

        return (
            <div>
                Button Example
            </div>
        );
    },
});

If you’ve used Guty blocks 2 you’ll recognize that this is the boilerplate block. If you aren’t using my fancy boilerplate, make sure you set up a way to do styles in our block. We’re going to style things up later.

Now, we’re going to create a folder components in the src folder, and add a ButtonWithLink.jsx file inside.

Button.jsx inside of our component folder, sibling to our block folder

Let us create a React function component that we’ll immediately export and use in our block…

import React from 'react';

const ButtonWithLink = (props) => {
    return (
        <div>
            <a href="#" class="btn btn--primary">
                This will eventually be a button!
            </a>
        </div>
    )
}

export default ButtonWithLink;

Great, now we have a link. Let’s style it:

// button-example.view.scss

.wp-block-test-button-example .btn {
    display: inline-block;
    padding: .5em 2em;
    background: #444;
    color: #fff;
    border: 2px solid #444;
    border-radius: 4px;
    transition: color 300ms, background 300ms;
    text-decoration: none;
}

.wp-block-test-button-example .btn:hover,
.wp-block-test-button-example .btn:active,
.wp-block-test-button-example .btn:focus {
    color: #444;
    background: #fff;
}

Alrighty. Now we have a button that we can use in blocks that we make by importing it in our block and calling it!

// button-example.js
...

import ButtonWithLink from '../components/ButtonWithLink.jsx';

edit(props) {
    const { className, setAttributes } = props;
    // const {  } = props.attributes;

    return [
        <InspectorControls>
            <div style={{padding: '1em 0'}}>
                Options
            </div>
        </InspectorControls>,
        <div className={className}>
            <ButtonWithLink />
        </div>,
    ];
},

...

Setting up attributes and update functions

So we need a common way for this ButtonWithLink to update the attributes of its parent. We also need to pass it attributes and populate values.

So lets start by creating our attributes in button-example.js:

attributes: {
    buttonText: {
        type: String,
    },
    buttonUrl: {
        url: string
    }
},

Now, we need to think about how we’re going to keep our updating functions general enough so that we could reuse this component in any block.

Here’s a way to do it: we will have a generic update function that takes a key and a value from the button component and update attributes. Also, I’m going to pass the button components the attributes while we’re here…

edit(props) {
    const { className, setAttributes } = props;
    const { buttonText, buttonUrl } = props.attributes;

    function updateAttr(key, value) {
        setAttributes({
            [key]: value,
        });
    }

    return [
        <InspectorControls>
            <div style={{padding: '1em 0'}}>
                Options
            </div>
        </InspectorControls>,
        <div className={className}>
            <ButtonWithLink
                url={buttonUrl}
                text={buttonText}
            />
        </div>,
    ];
},

Okay, so how do we now update those attributes?

The tricky part is that there might be more than one button in a block, and each button is going to need to update different things in the attributes.

So, I’m going to say that this component should have an onButtonTextChange and an onUrlChange. When those are called, I will then instruct my block where to place the values I’m getting from these change functions. First, this is what my ButtonWithLink Should look like:

// ButonWithLink.jsx

import React from 'react';

const {
    RichText,
    URLInputButton
} = wp.editor;

const ButtonWithLink = (props) => {
    return (
        <div>
            <RichText
                className="btn btn--primary"
                value={props.text}
                placeholder="Place your button text here!"
                onChange={props.onButtonTextChange}
            />
            <URLInputButton
                url={props.url}
                onChange={props.onURLChange}
            />
        </div>
    )
}

export default ButtonWithLink;

Next, we set up our component in our block:

...
<ButtonWithLink
    text={buttonText}
    url={buttonUrl}
    onButtonTextChange={val => updateAttribute('buttonText', val)}
    onURLChange={val => updateAttribute('buttonUrl', val)}
/>
...

This way we can tell which attribute to update in the block, and we don’t have to hard-code the ‘buttonText’ and ‘buttonUrl’ into the component.

By the way, if you load this in the editor, you’ll see why URLInput is so awesome. It auto-suggests posts and pages. Super nice!

Url input auto-suggesting 'sample page'
The sweetness of Gutenberg pre-made components!

Making a View for this Reusable Component

Alright, so we have a Button component working. We have a way to edit text and set up a corresponding url. Now we need to make a way to save this in our save().

I stole the following idea from RichText. Gutenberg already has a corresponding RichText component for views. You simply use RichText.Content.

How? We do the following in ButtonWithLink.

ButtonWithLink.View = (props) => {
    return (
        <div>
            <RichText.Content
                className="btn btn--primary"
                value={props.text}
                tagName="a"
                href={props.url}
            />
        </div>
    )
}

What are we doing here? Well, in Javascript functions are objects. So we are saving another function onto the ButtonWithLink function/object. We can access it now as <ButtonWithLink.View />. It’s a nice way to encapsulate our button into one import.

Now we make sure to change our save function in our block.

save(props) {
    // const className = getBlockDefaultClassName('test/button-example'); // For use with say, BEM
    const { buttonText, buttonUrl } = props.attributes;

    return (
        <div>
            <ButtonWithLink.View
                text={buttonText}
                url={buttonUrl}
            />
        </div>
    );
},

Let me know what you think!

I think in the near future I’ll do another example of this with images and clickable/editable images in the editor.

For now, let me know what you think! Hit me up @jschof or in the comments below.

Leave a Reply

Your email address will not be published. Required fields are marked *