Creating a Custom Gutenberg Block for Bootstrap Containers

In this post, we will break down the creation of a custom Gutenberg block that integrates Bootstrap 5 container classes using react-select for a rich user interface. The block allows users to select container and padding classes through the block inspector, enhancing the flexibility of content layout in WordPress. Let’s dive into the code step-by-step.

Importing Dependencies

// Importing the necessary dependencies and options
import ReactSelect from 'react-select'; // Importing ReactSelect for multi-select dropdowns, npm install react-select
import { paddingOptions, containerClasses } from '../columns/ColumnOptions'; // Importing options for padding and container classes
// those import variables are arrays of objects paddingOptions = [ { label: 'pt-0', value:'pt-0'}, etc. 
// you could add more, do less, its up to you, in this example I'm choosing to add container and padding options to the container, you would do whatever works best for yourself. 

Here, we are importing react-select for the multi-select dropdown component and the paddingOptions and containerClasses which contain the options for the user to select from.

Defining the Block Function

// Defining the main function for the custom Gutenberg block
export default function gutenbergBlocksBootstrapContainer(wp) {
    // Extracting necessary utilities from the wp object
    const { registerBlockType } = wp.blocks; // Function to register a block
    const { __ } = wp.i18n; // Localization function
    const { InspectorControls, InnerBlocks } = wp.blockEditor; // Block editor components
    const { PanelBody } = wp.components; // UI components
    const { useState, useEffect, Fragment } = wp.element; // React hooks and Fragment component

This function serves as the main entry point for our custom block. We extract necessary WordPress and React utilities from the wp object passed to this function.

Edit Function

The edit function is crucial as it defines how the block behaves and appears in the editor. Let’s explore its components:

// Edit function defining the block's backend behavior and UI
const edit = ({ attributes, setAttributes }) => {
    const { containerClass = '', paddingClass = '' } = attributes; // Destructuring attributes

    // Setting up state hooks for managing selected classes
    const [selectedContainerClasses, setSelectedContainerClasses] = useState(
        containerClass.split(' ').map((value) => containerClasses.find((option) => option.value === value)).filter(Boolean)
    );
    const [selectedPaddingClasses, setSelectedPaddingClasses] = useState(
        paddingClass.split(' ').map((value) => paddingOptions.find((option) => option.value === value)).filter(Boolean)
    );

We use the useState hook to manage the state of selected container and padding classes. This ensures that any changes made by the user are immediately reflected in the block’s attributes.

// useEffect hooks to update attributes when selected classes change
useEffect(() => {
    setAttributes({ containerClass: selectedContainerClasses.map((option) => option.value).join(' ') });
}, [selectedContainerClasses]);

useEffect(() => {
    setAttributes({ paddingClass: selectedPaddingClasses.map((option) => option.value).join(' ') });
}, [selectedPaddingClasses]);

The useEffect hooks ensure that whenever the selected container or padding classes change, the block’s attributes are updated accordingly.

// Handlers for when selection changes
const handleContainerClassChange = (newValue) => {
    setSelectedContainerClasses(newValue || []);
};

const handlePaddingClassChange = (newValue) => {
    setSelectedPaddingClasses(newValue || []);
};

These handler functions update the state when the user selects new options from the dropdowns.

    return (
        <Fragment>
            <InspectorControls>
                <PanelBody title={__('Container Settings')}>
                    <ReactSelect
                        isMulti
                        value={selectedContainerClasses}
                        options={containerClasses}
                        onChange={handleContainerClassChange}
                        className="react-select-container"
                        classNamePrefix="react-select"
                    />
                </PanelBody>
                <PanelBody title={__('Padding Settings')}>
                    <ReactSelect
                        isMulti
                        value={selectedPaddingClasses}
                        options={paddingOptions}
                        onChange={handlePaddingClassChange}
                        className="react-select-padding"
                        classNamePrefix="react-select"
                    />
                </PanelBody>
            </InspectorControls>
            <div className={`${selectedContainerClasses.map((option) => option?.value).join(' ')} ${selectedPaddingClasses.map((option) => option?.value).join(' ')}`}>
                <InnerBlocks
                    allowedBlocks={['bs5/bootstrap-row', 'bs5/bootstrap-grid']}
                    template={[['bs5/bootstrap-row']]}
                    renderAppender={InnerBlocks.ButtonBlockAppender}
                />
            </div>
        </Fragment>
    );
};

The InspectorControls component renders the settings panel in the block editor sidebar. We use PanelBody to group related controls together. The ReactSelect component provides a user-friendly interface for selecting multiple container and padding classes. The InnerBlocks component allows users to add child blocks within our custom container, restricted to specific block types.

Save Function

The save function determines the block’s HTML output on the frontend.

// Save function defining the block's frontend output
const save = ({ attributes }) => {
    const { containerClass, paddingClass } = attributes; // Destructuring attributes

    return (
        <div className={`${containerClass} ${paddingClass}`}>
            <InnerBlocks.Content />
        </div>
    );
};

In the save function, we simply render a div element with the selected container and padding classes, and include any inner blocks added by the user.

Registering the Block

Finally, we register the block with WordPress.

    // Registering the block type
    registerBlockType('bs5/bootstrap-container', {
        title: __('Bootstrap Container'), // Block title
        icon: 'layout', // Block icon
        category: 'layout', // Block category
        attributes: {
            containerClass: {
                type: 'string',
                default: 'container', // Default container class
            },
            paddingClass: {
                type: 'string',
                default: '', // Default padding class
            },
        },
        edit, // Edit function
        save, // Save function
    });
}

We define the block’s title, icon, category, and attributes. The edit and save functions are passed to manage the block’s behavior in the editor and on the frontend.

Complete Code

Here’s the complete code for our custom Gutenberg block:

import ReactSelect from 'react-select';
import { paddingOptions, containerClasses } from '../columns/ColumnOptions';

export default function gutenbergBlocksBootstrapContainer(wp) {
    const { registerBlockType } = wp.blocks;
    const { __ } = wp.i18n;
    const { InspectorControls, InnerBlocks } = wp.blockEditor;
    const { PanelBody } = wp.components;
    const { useState, useEffect, Fragment } = wp.element;

    const edit = ({ attributes, setAttributes }) => {
        const { containerClass = '', paddingClass = '' } = attributes;

        const [selectedContainerClasses, setSelectedContainerClasses] = useState(
            containerClass.split(' ').map((value) => containerClasses.find((option) => option.value === value)).filter(Boolean)
        );
        const [selectedPaddingClasses, setSelectedPaddingClasses] = useState(
            paddingClass.split(' ').map((value) => paddingOptions.find((option) => option.value === value)).filter(Boolean)
        );

        useEffect(() => {
            setAttributes({ containerClass: selectedContainerClasses.map((option) => option.value).join(' ') });
        }, [selectedContainerClasses]);

        useEffect(() => {
            setAttributes({ paddingClass: selectedPaddingClasses.map((option) => option.value).join(' ') });
        }, [selectedPaddingClasses]);

        const handleContainerClassChange = (newValue) => {
            setSelectedContainerClasses(newValue || []);
        };

        const handlePaddingClassChange = (newValue) => {
            setSelectedPaddingClasses(newValue || []);
        };

        return (
            <Fragment>
                <InspectorControls>
                    <PanelBody title={__('Container Settings')}>
                        <ReactSelect
                            isMulti
                            value={selectedContainerClasses}
                            options={containerClasses}
                            onChange={handleContainerClassChange}
                            className="react-select-container"
                            classNamePrefix="react-select"
                        />
                    </PanelBody>
                    <PanelBody title={__('Padding Settings')}>
                        <ReactSelect
                            isMulti
                            value={selectedPaddingClasses}
                            options={paddingOptions}
                            onChange={handlePaddingClassChange}
                            className="react-select-padding"
                            classNamePrefix="react-select"
                        />
                    </PanelBody>
                </InspectorControls>
                <div className={`${selectedContainerClasses.map((option) => option?.value).join(' ')} ${selectedPaddingClasses.map((option) => option?.value).join(' ')}`}>
                    <InnerBlocks
                        allowedBlocks={['bs5/bootstrap-row', 'bs5/bootstrap-grid']}
                        template={[['bs5/bootstrap-row']]}
                        renderAppender={InnerBlocks.ButtonBlockAppender}
                    />
                </div>
            </Fragment>
        );
    };

    const save = ({ attributes }) => {
        const { containerClass, paddingClass } = attributes;

        return (
            <div className={`${containerClass} ${paddingClass}`}>
                <InnerBlocks.Content />
            </div>
        );
    };

    registerBlockType('bs5/bootstrap-container', {
        title: __('Bootstrap Container'),
        icon: 'layout',
        category: 'layout',
        attributes: {
            containerClass: {
                type: 'string',
                default: 'container',
            },
            paddingClass: {
                type: 'string',
                default: '',
            },
        },
        edit,
        save,
    });
}

In this blog post, we’ve detailed the creation of a custom Gutenberg block for Bootstrap containers. We broke down the code into manageable sections, explaining the purpose and functionality of each part. The final code snippet encapsulates the entire block, ready to be used in your WordPress projects. Stay tuned for more posts in this series as we continue to explore and create powerful custom blocks!