A screenshot of the WordPress editor showing the Workshop Testimonial Block that we're making

Custom Block Building Workshop

| , ,

This is a written version of a workshop given most recently at WordCamp Rochester 2024. The workshop is more hands-on, but my hope is that the code and explanations here are detailed enough to help you in creating your first custom block.

The code for the plugin is available on my GitHub profile, and you can walk through the commits to see the order in which the plugin is built.

This workshop assumes a basic understanding of PHP and JavaScript, as well as a code editor and somewhere to test the code, such as a test WordPress site, or a local development environment. It does not require the use of React or another JavaScript framework, nor does it require build tools.

I won’t be describing every single line-by-line change, but if you follow along and view the commits, you should be able to see everything that changes for each step.

Setting Up the Plugin

Start by creating a folder in your wp-content/plugins folder in WordPress. You can name the folder whatever you want, though I’m going with testimonial-block-workshop.

Create plugin files

Add new files to plugin folder

In that folder, create three files: testimonial-block.php, testimonial-block.js, and testimonial-block.css. These files are going to hold the code to register the plugin, build the block, and style the block, respectively.

Add plugin header code

Add plugin header code

The following code, placed into testimonial-block.php, will let the plugin show up in the site dashboard. This starts the PHP file and uses PHP comments as a way to register the plugin. We’re providing a name, description, version, and author. All of these show up in the dashboard, and the version is important for when updates come in the future.

<?php
/**
 * Plugin Name: Testimonial Block Workshop
 * Description: Basic Custom Testimonial Block with no build tools needed - Workshop Demo
 * Version: 1.0.0
 * Author: david wolfpaw
 */

After you save this code, you should be able to go to the plugin dashboard and see your plugin! Activate it and make it look all highlighted and happy.

A screenshot of the WordPress plugin dashboard, showing the Testimonial Block Workshop plugin activated.

Add exit code and define version constant

Add exit code and define version constant

Now we’re gonna add code to the PHP file that will ensure that the plugin file doesn’t get accessed directly, and defines a constant for the version of the plugin. We’ll be using this later.

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}
// Define plugin version
define( 'TESTIMONIAL_BLOCK_VERSION', '1.0.0' );

Registering the Block

Now that we’re setup, it’s time to register the block, so that it’s visible in the WordPress editor.

Register PHP for the block

register PHP for the block

This code registers the JavaScript file for the plugin, which is where we’ll be doing the most work. Note that we first register the file with wp_register_script, and then we use register_block_type and tell it which script file to use.

// Function to register the block and enqueue scripts/styles
function register_testimonial_block() {
	// Register the block's JavaScript file
	wp_register_script(
		'testimonial-block-script', // Unique script handle
		plugins_url( 'testimonial-block.js', __FILE__ ), // URL of where the block file exists
		array( 'wp-blocks', 'wp-block-editor' ), // Scripts that should load before this script
		TESTIMONIAL_BLOCK_VERSION, // Version of the plugin script (optional)
		true, // Is script in HTML Footer (optional)
	);
	// Register the block type with the scripts and styles
	register_block_type(
		// Block namespace and name
		'workshop/testimonial-block',
		// Array of arguments for the block
		array(
			'editor_script' => 'testimonial-block-script',
		)
	);
}
add_action( 'init', 'register_testimonial_block' );

Register JS for the block

register JS for the block

In the JavaScript file, we likewise register the block. We use the same namespace and name for the block that we used in the PHP file (workshop/testimonial-block).

The main things that we’re doing are adding a title and icon, as well as choosing the category of blocks that it will go into for when we select it. The edit and save functions right now are just returning a paragraph tag. The goal at this point is to test to ensure that we can see the block in the editor, and that when we save it, something will display on the frontend of the site.

(function (blocks, element) {
    const { registerBlockType } = blocks;
    const { createElement } = element;
    // Step 1: Register the block
    registerBlockType('workshop/testimonial-block', {
        title: 'Workshop Testimonial Block', // Block title
        icon: 'admin-comments', // Icon from WordPress dashicons
        category: 'common', // Category in the block inserter
        edit: function () {
            return createElement('p', null, 'Workshop Testimonial Block'); // Placeholder text in editor
        },
        save: function () {
            return createElement('p', null, 'Workshop Testimonial Block'); // Placeholder text on frontend
        }
    });
})(window.wp.blocks, window.wp.element);

With both of these files saved, you should be able to go to edit a post or page and find your block.

A screenshot of the WordPress editor showing the Workshop Testimonial Block that we're making

Creating Fields to Edit Block

Alright, now that we’ve gotten really set up, it’s time to create some fields for our block. We’re going to create some text fields, an image upload button, and color selectors.

Add editable text fields

Add editable text fields

The following code adds some of the other constants that we need to make text controls, and adds two text fields: testimonialText and authorName to unsurprisingly store the testimonial text, and the author name.

(function (blocks, element, blockEditor, components) {
    const { registerBlockType } = blocks;
    const { TextControl } = components; // Import the TextControl component
    const { createElement, Fragment } = element;

    // Step 1: Register the block
    registerBlockType('workshop/testimonial-block', {
        title: 'Workshop Testimonial Block', // Block title
        icon: 'admin-comments', // Icon from WordPress dashicons
        category: 'common', // Category in the block inserter
        // Step 2: Add Editable Text Fields
        attributes: {
            testimonialText: { type: 'string', default: '' }, // Testimonial text
            authorName: { type: 'string', default: '' }, // Author name
        },
        edit: function ({ attributes, setAttributes }) {
            const { testimonialText, authorName } = attributes;
            return createElement(
                Fragment,
                null,
                // Add TextControl for Testimonial Text
                createElement(TextControl, {
                    label: 'Testimonial Text',
                    value: testimonialText,
                    onChange: (value) => setAttributes({ testimonialText: value }),
                    placeholder: 'Enter the testimonial text…'
                }),
                // Add TextControl for Author Name
                createElement(TextControl, {
                    label: 'Author Name',
                    value: authorName,
                    onChange: (value) => setAttributes({ authorName: value }),
                    placeholder: 'Enter the author’s name…'
                })
            );
        },
        save: function ({ attributes }) {
            return createElement('p', null, 'Custom Testimonial Block'); // Placeholder text on frontend
        }
    });
})(window.wp.blocks, window.wp.element, window.wp.blockEditor, window.wp.components);

We are creating the elements that you’ll be able to see on the backend of the site, including labels and placeholders for those fields.

a screenshot of the testimonial block backend, showing the editable text fields

Display text on frontend

Display text on frontend

The save function will affect what saves tot he database to display on the frontend of the site. We’ve replaced the placeholder paragraph with a div that holds a blockquote and bolded paragraph tag for the testimonial text and the author name.

save: function ({ attributes }) {
    // Step 3: Create Elements to Display Text on Frontend
    const { testimonialText, authorName } = attributes;
    return createElement('div', null,
        createElement('blockquote', null, testimonialText),
        createElement('p', { style: { fontWeight: 'bold' } }, authorName)
    );
}
a screenshot of the testimonial block frontend, showing the text fields

Add image upload controls

Add image upload controls

Same as before, we add the constants needed for the media uploader. We’re going to be saving the URL of the image to use on the frontend. We’re also creating a button that says “Upload Author Image” if no image is set, and changes to “Change Author Image” if an image is already present.

(function (blocks, element, blockEditor, components) {
    const { registerBlockType } = blocks;
    // Step 4: Add Button and Media Upload Components
    const { TextControl, Button } = components; // Import the Button Component
    const { MediaUpload, MediaUploadCheck } = blockEditor; // Import the Media Upload Components
    const { createElement, Fragment } = element;

    registerBlockType('workshop/testimonial-block', {
        title: 'Workshop Testimonial Block', // Block title
        icon: 'admin-comments', // Icon from WordPress dashicons
        category: 'common', // Category in the block inserter

        attributes: {
            testimonialText: { type: 'string', default: '' }, // Testimonial text
            authorName: { type: 'string', default: '' }, // Author name
            // Step 4: Add Image URL Attribute
            authorImage: { type: 'string', default: '' }, // Author image URL
        },
        edit: function ({ attributes, setAttributes }) {
            // Step 4: Add Image URL Attribute
            const { testimonialText, authorName, authorImage } = attributes;

            return createElement(
                Fragment,
                null,
                // Step 4: Add Media Upload for Author Image
                // Media Upload for Author Image
                createElement(MediaUploadCheck, {},
                    createElement(MediaUpload, {
                        onSelect: (media) => setAttributes({ authorImage: media.url }),
                        allowedTypes: ['image'],
                        value: authorImage,
                        render: ({ open }) => createElement(Button, { onClick: open, className: 'is-primary' }, authorImage ? 'Change Author Image' : 'Upload Author Image')
                    })
                ),
                // Display Uploaded Image in Editor
                authorImage && createElement('img', { src: authorImage, alt: 'Author Image' }),
                // Add TextControl for Testimonial Text
                createElement(TextControl, {
                    label: 'Testimonial Text',
            );
        },
a screenshot of the testimonial block backend, showing the image upload

Display image on frontend

Display image on frontend

Same as with the text, we’re now using the same code to create an image element on the frontend to display our author image.

save: function ({ attributes }) {
    const { testimonialText, authorName, authorImage } = attributes;
    return createElement('div', null,
        // Step 5: Render Image on Frontend
        authorImage && createElement('img', { src: authorImage, alt: 'Author Image' }),
        createElement('blockquote', null, testimonialText),
        createElement('p', { style: { fontWeight: 'bold' } }, authorName)
    );
}
a screenshot of the testimonial block frontend, showing the uploaded image and text

Add color control settings

Add color control settings

With the following four changes, we’re adding the ability to create a color panel like the color selectors that appear in other WordPress blocks. We’re saving the settings for a background color and text color for the testimonial block.

// Step 6: Import the Panel Color Settings
const { MediaUpload, MediaUploadCheck, PanelColorSettings } = blockEditor;


// Step 6: Add Attributes for Background and Text Colors
backgroundColor: { type: 'string', default: '#ffffff' }, // Background color
textColor: { type: 'string', default: '#000000' }, // Text color
            

edit: function ({ attributes, setAttributes }) {
    // Step 6: Add Attributes for Background and Text Colors
    const { testimonialText, authorName, authorImage, backgroundColor, textColor } = attributes;


// Step 6: Panel for Color Settings
createElement(PanelColorSettings, {
    title: 'Color Settings',
    initialOpen: true,
    colorSettings: [
        {
            value: backgroundColor,
            onChange: (value) => setAttributes({ backgroundColor: value }),
            label: 'Background Color',
        },
        {
            value: textColor,
            onChange: (value) => setAttributes({ textColor: value }),
            label: 'Text Color',
        }
    ],
a screenshot of the testimonial block backend, showing the color settings fields

Move image and color controls to Inspector Panel

Move image and color controls to Inspector Panel

Now that we’ve added more controls to our block, it’s starting to look a bit cluttered in the editor. At this point, I think that it’s reasonable to move these settings from the main block editor to the Inspector Panel that shows up on right sidebar when you have a specific block clicked.

We’re going to move the media upload button and color selectors to the sidebar, while keeping the text fields in the main editor for now. Spoiler: we’re going to move those later as well to tidy up our block view even more.

edit: function ({ attributes, setAttributes }) {
    const { testimonialText, authorName, authorImage, backgroundColor, textColor } = attributes;

    // Step 7: Move Settings into Inspector Controls
    return (
        createElement(Fragment,
            null,
            // InspectorControls Adds Settings in the Sidebar
            createElement(InspectorControls,
                null,
                // PanelBody for Testimonial Settings
                createElement(PanelBody,
                    { title: 'Testimonial Settings' },
                    // Media Upload for Author Image
                    createElement(MediaUploadCheck, {},
                        createElement(MediaUpload, {
                            onSelect: (media) => setAttributes({ authorImage: media.url }),
                            allowedTypes: ['image'],
                            value: authorImage,
                            render: ({ open }) => createElement(Button, { onClick: open, isPrimary: true }, authorImage ? 'Change Author Image' : 'Upload Author Image')
                        })
                    )
                ),
                // Panel for Color Settings
                createElement(PanelColorSettings, {
                    title: 'Color Settings',
                    initialOpen: true,
                    colorSettings: [
                        {
                            value: backgroundColor,
                            onChange: (value) => setAttributes({ backgroundColor: value }),
                            label: 'Background Color',
                        },
                        {
                            value: textColor,
                            onChange: (value) => setAttributes({ textColor: value }),
                            label: 'Text Color',
                        }
                    ],
                })
            ),

            // Step 8: Make div to Display Testimonial in Editor
            createElement('div',
                null,
                // Display Uploaded Image in Editor
                authorImage && createElement('img', { src: authorImage, alt: 'Author Image' }),
                // Add TextControl for Testimonial Text
                createElement(TextControl, {
                    label: 'Testimonial Text',
                    value: testimonialText,
                    onChange: (value) => setAttributes({ testimonialText: value }),
                    placeholder: 'Enter the testimonial text…'
                }),
                // Add TextControl for Author Name
                createElement(TextControl, {
                    label: 'Author Name',
                    value: authorName,
                    onChange: (value) => setAttributes({ authorName: value }),
                    placeholder: 'Enter the author’s name…'
                })
            )
        )
    );
},
a screenshot of the testimonial block backend, showing the media upload button and color settings fields moved into the inspector panel sidebar

Display color changes in editor

Display color changes in editor

Now that we’ve moved the color and media settings to the sidebar, we can add some attributes to the div in the editor. Instead of an attribute set of null as it was before, we’re adding a JSON array that includes styles, and a class name.

This will make inline style changes that will update when a new color is selected for either the text or background.

// Step 9: Display Color Changes in Editor
{
    style: {
        color: textColor,
        backgroundColor: backgroundColor
    },
    className: 'custom-testimonial-block'
},
a screenshot of the testimonial block backend, showing custom colors set

Display color changes on frontend

Display color changes on frontend

This should look basically the same as the previous step. We’re making the save function return a div that has some style attributes and a class name. The class name we’ll be using when we do more styling at the end.

// Step 10: Display Color Changes on Frontend
const { testimonialText, authorName, authorImage, backgroundColor, textColor } = attributes;
return createElement('div',
    {
        style: {
            color: textColor,
            backgroundColor: backgroundColor
        },
        className: 'custom-testimonial-block'
    },
a screenshot of the testimonial block frontend, showing custom colors set

Move text controls to Inspector Panel

Move text controls to Inspector Panel

I had an idea that I’d be moving the text controls to the Inspector Panel in the sidebar as well. While it’s not editable right where it’d be on the page, I think that it looks and acts cleaner this way, with less clutter in the display and all of the things that you’ll actually edit next to one another.

This step involves moving the controls into the InspectorControls element, as well as creating new elements in the div in the main page editor to display the updated blockquote and paragraph tags.

// Step 11: Move Text Controls into Inspector Panel
// Add TextControl for Testimonial Text
createElement(TextControl, {
    label: 'Testimonial Text',
    value: testimonialText,
    onChange: (value) => setAttributes({ testimonialText: value }),
    placeholder: 'Enter the testimonial text…'
}),
// Add TextControl for Author Name
createElement(TextControl, {
    label: 'Author Name',
    value: authorName,
    onChange: (value) => setAttributes({ authorName: value }),
    placeholder: 'Enter the author’s name…'
}),


// Step 11: Move Text Controls into Inspector Panel
createElement('blockquote', null, testimonialText),
createElement('p', { style: { fontWeight: 'bold' } }, authorName)
a screenshot of the testimonial block backend, showing all editable fields moved to the inspector panel sidebar

Block Alignment and Styles

We’re almost at the end! Now that we’ve made it this far, it’s time to add a bit of style to our testimonials. We’re going to do this in two ways: add support to allow you to change the alignment of the testimonial block, and add some custom styles for it.

Add block alignment support

Add block alignment support

We’re adding a new parameter to our block: supports. This will let us use an existing feature in the block editor to control alignment. I’ve set all five core options, to allow us to float the testimonial block, or to make it wide or full width on the page. We set an attribute with whatever is saved to allow it to display properly.

Unlike with the custom attributes that we created, we don’t need to add this to our edit or save functions.

// Step 12: Add alignment support
supports: {
    align: ['left', 'center', 'right', 'wide', 'full'], // Supports alignment options
},
attributes: {
    testimonialText: { type: 'string', default: '' }, // Testimonial text
    authorName: { type: 'string', default: '' }, // Author name
    authorImage: { type: 'string', default: '' }, // Author image URL
    backgroundColor: { type: 'string', default: '#ffffff' }, // Background color
    textColor: { type: 'string', default: '#000000' }, // Text color
    blockAlignment: { type: 'string', default: 'none' }, // Block alignment
},
a screenshot of the testimonial block backend, showing the block alignment options

Register block stylesheet

Register block stylesheet

Finally touching that PHP file again! The main things of note here are that we are registering the stylesheet that we already created, testimonial-block.css, and telling WordPress where it can be found. We are then adding style_handles to the register_block_type function. This is an array, since there could be more than one stylesheet added.

You do have the ability to specify stylesheets for the frontend and backend of the site separately, but in our case we’re going to have the same one apply, since it isn’t too hard on this simple block to style to look the same to visitors as it does while editing.

// Register the block's styles
wp_register_style(
	'testimonial-block-style', // Unique stylesheet handle
	plugins_url( 'testimonial-block.css', __FILE__ ), // URL of where the stylesheet file exists
	array( 'wp-edit-blocks' ), // Stylesheets that should load before this script
	TESTIMONIAL_BLOCK_VERSION // Version of the plugin script (optional)
);
// Register the block type with the scripts and styles
register_block_type(
	// Block namespace and name
	'workshop/testimonial-block',
	// Array of arguments for the block
	array(
		'editor_script' => 'testimonial-block-script',
		'style_handles' => array( 'testimonial-block-style' ),
	)
);

Add block styles

Add block styles

And now we’re finally using our CSS file!

Nothing very special of note here. This code:

  • makes a box and adds a border to it (same color as the text color)
  • centers the image and text
  • rounds the image to a circle and adds a border to it as well (same color as the text color)
  • makes the block quote look a bit fancy with a large quotation mark
  • bolds the author name
.custom-testimonial-block {
    text-align: center;
    padding: 20px;
    border-radius: 20px;
    border: 3px solid;
    font-size: 1em;
}
.custom-testimonial-block img {
    border-radius: 50%;
    border: 3px solid;
    background-color: #fff;
}
.custom-testimonial-block blockquote {
    font-size: 1.5em;
    padding: 50px;
    font-style: italic;
    position: relative;
}
.custom-testimonial-block blockquote::before {
    content: '“';
    font-family: sans-serif;
    font-size: 4em;
    line-height: 1;
    position: absolute;
    left: 10px;
    top: -10px;
}
.custom-testimonial-block p {
    font-size: 1em;
    font-weight: bold;
}

You probably noticed that the author name was already bolded before. That’s because we originally used this:

createElement('p', { style: { fontWeight: 'bold' } }, authorName)

Now we removed the style attributes, to get this:

createElement('p', null, authorName)

The main reason that I made this change is just to keep styling consistently in one area, so that we can more easily make changes to it in the future if we want.

Final Thoughts

I realize that was a lot to go through at once. I don’t blame you if it takes a few tries (it took more than a few for me!) or requires a few sessions and examples to get it down.

This is just one way of building a custom block, and not how most that you will see will look. That’s because instead of using the fancy and very helpful @wordpress/create-block tooling, I opted for something that would not require you to learn any build tools, how to use the command line, or having a separation of source and build files.

From here I want you to go out and create custom blocks that are useful for your own sites, and share them with the world! I’ll be happy to showcase some if you reply with a link to what you end up building.

One response to “Custom Block Building Workshop”

Mentions

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.

To respond on your own website, enter the URL of your response which should contain a link to this post’s permalink URL. Your response will then appear (possibly after moderation) on this page. Want to update or remove your response? Update or delete your post and re-enter your post’s URL again. (Find out more about Webmentions.)

🌙 ☀️