Creating a Custom Gutenberg Block for Bootstrap Grid Columns

In this blog post, we will explore how to create a custom Gutenberg block for Bootstrap grid columns. This block allows users to configure various column settings, such as CSS grid, overflow, padding, order, and experimental options, using react-select for a flexible and user-friendly interface. Additionally, users can set the background color and add or remove inner blocks dynamically. Let’s break down the code step-by-step.

Importing Dependencies

// Importing the necessary dependencies and options
import ReactSelect from 'react-select'; // Importing ReactSelect for multi-select dropdowns
import { cssGridColumnOptions, overflowOptions, paddingOptions, orderOptions, exoticColumnOptions, cssGridStartOptions } from './ColumnOptions'; // Importing options for various column settings

Here, we import react-select for the multi-select dropdown component and the various options for column settings, such as CSS grid, overflow, padding, order, exotic column, and CSS grid start options.

Defining the Block Function

// Defining the main function for the custom Gutenberg block
export default function gutenbergBlocksBootstrapGridColumn(wp) {
    // Extracting necessary utilities from the wp object
    const { __ } = wp.i18n; // Localization function
    const { useEffect, useState, useRef } = wp.element; // React hooks and useRef component
    const { InspectorControls, InnerBlocks, BlockControls, useBlockProps } = wp.blockEditor; // Block editor components
    const { PanelBody, ToolbarButton, ColorPalette } = wp.components; // UI components
    const { registerBlockType } = wp.blocks; // Function to register a block
    const { select, dispatch } = wp.data; // Data handling functions

    // Registering the block type
    registerBlockType('bs5/bootstrap-grid-column', {
        apiVersion: 2,
        title: __('Bootstrap Grid Column'), // Block title
        icon: 'layout', // Block icon
        category: 'layout', // Block category
        parent: ['bs5/bootstrap-grid'], // Parent block
        supports: {
            inserter: false, // Prevent manual insertion outside the parent block
        },
        attributes: {
            columnClasses: { type: 'array', default: [] }, // Attribute for column classes
            backgroundColor: { type: 'string', default: '' }, // Attribute for background color
        },
        edit: ({ attributes, setAttributes, clientId }) => {
            const { columnClasses, backgroundColor } = attributes; // Destructuring attributes
            const blockRef = useRef(); // Reference for the block element
            const blockProps = useBlockProps(); // Block properties for block editor

            // Function to get initial state based on columnClasses attribute
            const getInitialState = (options) => options.filter((option) => columnClasses.includes(option.value));

            // Initializing state for each class type based on columnClasses attribute
            const [selectedCssGridColumnClasses, setSelectedCssGridColumnClasses] = useState(getInitialState(cssGridColumnOptions));
            const [selectedOverflowClasses, setSelectedOverflowClasses] = useState(getInitialState(overflowOptions));
            const [selectedPaddingClasses, setSelectedPaddingClasses] = useState(getInitialState(paddingOptions));
            const [selectedOrderColumnClasses, setSelectedOrderColumnClasses] = useState(getInitialState(orderOptions));
            const [selectedExoticColumnClasses, setSelectedExoticColumnClasses] = useState(getInitialState(exoticColumnOptions));
            const [selectedCssGridStartClasses, setSelectedCssGridStartClasses] = useState(getInitialState(cssGridStartOptions));

            // useEffect to update columnClasses attribute based on state changes
            useEffect(() => {
                const combinedClasses = [
                    ...selectedCssGridColumnClasses,
                    ...selectedOverflowClasses,
                    ...selectedPaddingClasses,
                    ...selectedOrderColumnClasses,
                    ...selectedExoticColumnClasses,
                    ...selectedCssGridStartClasses
                ].map((option) => option.value).filter(Boolean);

                setAttributes({ columnClasses: combinedClasses });
            }, [selectedCssGridColumnClasses, selectedOverflowClasses, selectedPaddingClasses, selectedOrderColumnClasses, selectedExoticColumnClasses, selectedCssGridStartClasses]);

            // Handler function to update state for class selectors
            const handleClassChange = (setter) => (newValue) => {
                setter(newValue || []);
            };

            // Ensure value prop passed to ReactSelect is in correct format
            const getSelectedOptions = (selectedClasses, options) => options.filter((option) => selectedClasses.map((opt) => opt.value).includes(option.value));

            return (
                <>
                    <BlockControls>
                        <ToolbarButton
                            icon="plus"
                            label={__('Add Inner Block')}
                            onClick={() => {
                                const { insertBlock } = dispatch('core/block-editor');
                                const { getBlock } = select('core/block-editor');
                                const block = getBlock(clientId);
                                const newBlock = wp.blocks.createBlock('core/paragraph', { placeholder: 'Enter paragraph text...' });
                                insertBlock(newBlock, block.innerBlocks.length, clientId);
                            }}
                        />
                    </BlockControls>
                    <InspectorControls>
                        <PanelBody title={__('Grid Column Settings')}>
                            <ReactSelect
                                isMulti
                                value={getSelectedOptions(selectedCssGridColumnClasses, cssGridColumnOptions)}
                                options={cssGridColumnOptions}
                                onChange={handleClassChange(setSelectedCssGridColumnClasses)}
                                className="react-select-container"
                                classNamePrefix="react-select"
                            />
                        </PanelBody>
                        <PanelBody title={__('Overflow Settings')}>
                            <ReactSelect
                                isMulti
                                value={getSelectedOptions(selectedOverflowClasses, overflowOptions)}
                                options={overflowOptions}
                                onChange={handleClassChange(setSelectedOverflowClasses)}
                                className="react-select-container"
                                classNamePrefix="react-select"
                                style={{ marginTop: '10px' }}
                            />
                        </PanelBody>
                        <PanelBody title={__('Padding Settings')}>
                            <ReactSelect
                                isMulti
                                value={getSelectedOptions(selectedPaddingClasses, paddingOptions)}
                                options={paddingOptions}
                                onChange={handleClassChange(setSelectedPaddingClasses)}
                                className="react-select-container"
                                classNamePrefix="react-select"
                                style={{ marginTop: '10px' }}
                            />
                        </PanelBody>
                        <PanelBody title={__('Order Settings')}>
                            <ReactSelect
                                isMulti
                                value={getSelectedOptions(selectedOrderColumnClasses, orderOptions)}
                                options={orderOptions}
                                onChange={handleClassChange(setSelectedOrderColumnClasses)}
                                className="react-select-container"
                                classNamePrefix="react-select"
                                style={{ marginTop: '10px' }}
                            />
                        </PanelBody>
                        <PanelBody title={__('CSS Grid Start Settings')}>
                            <ReactSelect
                                isMulti
                                value={getSelectedOptions(selectedCssGridStartClasses, cssGridStartOptions)}
                                options={cssGridStartOptions}
                                onChange={handleClassChange(setSelectedCssGridStartClasses)}
                                className="react-select-container"
                                classNamePrefix="react-select"
                                style={{ marginTop: '10px' }}
                            />
                        </PanelBody>
                        <PanelBody title={__('Experimental Settings')}>
                            <small>These settings are experimental and may not work as expected.</small><br/>
                            <small>This will cause the column to be flush right or left against the edge of the window when on desktop</small>
                            <ReactSelect
                                isMulti
                                value={getSelectedOptions(selectedExoticColumnClasses, exoticColumnOptions)}
                                options={exoticColumnOptions}
                                onChange={handleClassChange(setSelectedExoticColumnClasses)}
                                className="react-select-container"
                                classNamePrefix="react-select"
                                isOptionDisabled={() => getSelectedOptions(selectedExoticColumnClasses, exoticColumnOptions).length >= 1}
                                style={{ marginTop: '10px' }}
                            />
                        </PanelBody>
                        <PanelBody title={__('Background Color')}>
                            <ColorPalette
                                value={backgroundColor}
                                onChange={(color) => setAttributes({ backgroundColor: color })}
                                enableAlpha
                            />
                        </PanelBody>
                    </InspectorControls>
                    <div
                        {...blockProps}
                        ref={blockRef}
                        style={{ backgroundColor: backgroundColor || undefined }}
                        className={`g-col ${[
                            ...selectedCssGridColumnClasses,
                            ...selectedOverflowClasses,
                            ...selectedPaddingClasses,
                            ...selectedOrderColumnClasses,
                            ...selectedExoticColumnClasses,
                            ...selectedCssGridStartClasses
                        ].map((option) => option.value).join(' ')}`}
                    >
                        <InnerBlocks
                            renderAppender={false} // Disable the default appender
                            template={[
                                ['core/heading', { placeholder: 'Enter heading text...' }],
                                ['core/paragraph', { placeholder: 'Enter paragraph text...' }]
                            ]}
                        />
                    </div>
                </>
            );
        },
        save: ({ attributes }) => {
            const { columnClasses, backgroundColor } = attributes;

            return (
                <div
                    className={`g-col ${columnClasses.join(' ')}`}
                    style={{ backgroundColor: backgroundColor || undefined }}
                >
                    <InnerBlocks.Content />
                </div>
            );
        },
    });
}

Understanding the Block Attributes and State Management

In the above code, we register a custom Gutenberg block named ‘Bootstrap Grid Column’. This block allows users to select various CSS grid classes, manage overflow, padding, order, experimental settings, and background color. Let’s break down how state management and attribute updates are handled:

// useState hooks for managing selected classes
const [selectedCssGridColumnClasses, setSelectedCssGridColumnClasses] = useState(getInitialState(cssGridColumnOptions));
const [selectedOverflowClasses, setSelectedOverflowClasses] = useState(getInitialState(overflowOptions));
const [selectedPaddingClasses, setSelectedPaddingClasses] = useState(getInitialState(paddingOptions));
const [selectedOrderColumnClasses, setSelectedOrderColumnClasses] = useState(getInitialState(orderOptions));
const [selectedExoticColumnClasses, setSelectedExoticColumnClasses] = useState(getInitialState(exoticColumnOptions));
const [selectedCssGridStartClasses, setSelectedCssGridStartClasses] = useState(getInitialState(cssGridStartOptions));

These state hooks are initialized using a helper function getInitialState, which filters the options based on the current columnClasses attribute.

// useEffect to update columnClasses attribute based on state changes
useEffect(() => {
    const combinedClasses = [
        ...selectedCssGridColumnClasses,
        ...selectedOverflowClasses,
        ...selectedPaddingClasses,
        ...selectedOrderColumnClasses,
        ...selectedExoticColumnClasses,
        ...selectedCssGridStartClasses
    ].map((option) => option.value).filter(Boolean);

    setAttributes({ columnClasses: combinedClasses });
}, [selectedCssGridColumnClasses, selectedOverflowClasses, selectedPaddingClasses, selectedOrderColumnClasses, selectedExoticColumnClasses, selectedCssGridStartClasses]);

The useEffect hook ensures that the columnClasses attribute is updated whenever any of the selected class states change.

Edit Function and Inspector Controls

The edit function defines the block’s behavior in the editor, including the settings panels and the block preview.

<InspectorControls>
    <PanelBody title={__('Grid Column Settings')}>
        <ReactSelect
            isMulti
            value={getSelectedOptions(selectedCssGridColumnClasses, cssGridColumnOptions)}
            options={cssGridColumnOptions}
            onChange={handleClassChange(setSelectedCssGridColumnClasses)}
            className="react-select-container"
            classNamePrefix="react-select"
        />
    </PanelBody>
    <PanelBody title={__('Overflow Settings')}>
        <ReactSelect
            isMulti
            value={getSelectedOptions(selectedOverflowClasses, overflowOptions)}
            options={overflowOptions}
            onChange={handleClassChange(setSelectedOverflowClasses)}
            className="react-select-container"
            classNamePrefix="react-select"
            style={{ marginTop: '10px' }}
        />
    </PanelBody>
    <PanelBody title={__('Padding Settings')}>
        <ReactSelect
            isMulti
            value={getSelectedOptions(selectedPaddingClasses, paddingOptions)}
            options={paddingOptions}
            onChange={handleClassChange(setSelectedPaddingClasses)}
            className="react-select-container"
            classNamePrefix="react-select"
            style={{ marginTop: '10px' }}
        />
    </PanelBody>
    <PanelBody title={__('Order Settings')}>
        <ReactSelect
            isMulti
            value={getSelectedOptions(selectedOrderColumnClasses, orderOptions)}
            options={orderOptions}
            onChange={handleClassChange(setSelectedOrderColumnClasses)}
            className="react-select-container"
            classNamePrefix="react-select"
            style={{ marginTop: '10px' }}
        />
    </PanelBody>
    <PanelBody title={__('CSS Grid Start Settings')}>
        <ReactSelect
            isMulti
            value={getSelectedOptions(selectedCssGridStartClasses, cssGridStartOptions)}
            options={cssGridStartOptions}
            onChange={handleClassChange(setSelectedCssGridStartClasses)}
            className="react-select-container"
            classNamePrefix="react-select"
            style={{ marginTop: '10px' }}
        />
    </PanelBody>
    <PanelBody title={__('Experimental Settings')}>
        <small>These settings are experimental and may not work as expected.</small><br/>
        <small>This will cause the column to be flush right or left against the edge of the window when on desktop</small>
        <ReactSelect
            isMulti
            value={getSelectedOptions(selectedExoticColumnClasses, exoticColumnOptions)}
            options={exoticColumnOptions}
            onChange={handleClassChange(setSelectedExoticColumnClasses)}
            className="react-select-container"
            classNamePrefix="react-select"
            isOptionDisabled={() => getSelectedOptions(selectedExoticColumnClasses, exoticColumnOptions).length >= 1}
            style={{ marginTop: '10px' }}
        />
    </PanelBody>
    <PanelBody title={__('Background Color')}>
        <ColorPalette
            value={backgroundColor}
            onChange={(color) => setAttributes({ backgroundColor: color })}
            enableAlpha
        />
    </PanelBody>
</InspectorControls>

The InspectorControls component renders various settings panels for configuring the block. We use PanelBody to group related controls together. The ReactSelect component provides a user-friendly interface for selecting multiple classes, while ColorPalette allows users to set the background color.

Save Function

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

save: ({ attributes }) => {
    const { columnClasses, backgroundColor } = attributes;

    return (
        <div
            className={`g-col ${columnClasses.join(' ')}`}
            style={{ backgroundColor: backgroundColor || undefined }}
        >
            <InnerBlocks.Content />
        </div>
    );
};

In the save function, we render a div element with the selected column classes and the background color, and include any inner blocks added by the user.

Complete Code

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

import ReactSelect from 'react-select';
import { cssGridColumnOptions, overflowOptions, paddingOptions, orderOptions, exoticColumnOptions, cssGridStartOptions } from './ColumnOptions';

export default function gutenbergBlocksBootstrapGridColumn(wp) {
    const { __ } = wp.i18n;
    const { useEffect, useState, useRef } = wp.element;
    const { InspectorControls, InnerBlocks, BlockControls, useBlockProps } = wp.blockEditor;
    const { PanelBody, ToolbarButton, ColorPalette } = wp.components;
    const { registerBlockType } = wp.blocks;
    const { select, dispatch } = wp.data;

    registerBlockType('bs5/bootstrap-grid-column', {
        apiVersion: 2,
        title: __('Bootstrap Grid Column'),
        icon: 'layout',
        category: 'layout',
        parent: ['bs5/bootstrap-grid'],
        supports: {
            inserter: false, // Prevent manual insertion outside the parent block
        },
        attributes: {
            columnClasses: { type: 'array', default: [] },
            backgroundColor: { type: 'string', default: '' },
        },
        edit: ({ attributes, setAttributes, clientId }) => {
            const { columnClasses, backgroundColor } = attributes;
            const blockRef = useRef();
            const blockProps = useBlockProps();

            // Function to get initial state based on columnClasses attribute
            const getInitialState = (options) => options.filter((option) => columnClasses.includes(option.value));

            // Initializing state for each class type based on columnClasses attribute
            const [selectedCssGridColumnClasses, setSelectedCssGridColumnClasses] = useState(getInitialState(cssGridColumnOptions));
            const [selectedOverflowClasses, setSelectedOverflowClasses] = useState(getInitialState(overflowOptions));
            const [selectedPaddingClasses, setSelectedPaddingClasses] = useState(getInitialState(paddingOptions));
            const [selectedOrderColumnClasses, setSelectedOrderColumnClasses] = useState(getInitialState(orderOptions));
            const [selectedExoticColumnClasses, setSelectedExoticColumnClasses] = useState(getInitialState(exoticColumnOptions));
            const [selectedCssGridStartClasses, setSelectedCssGridStartClasses] = useState(getInitialState(cssGridStartOptions));

            // useEffect to update columnClasses attribute based on state changes
            useEffect(() => {
                const combinedClasses = [
                    ...selectedCssGridColumnClasses,
                    ...selectedOverflowClasses,
                    ...selectedPaddingClasses,
                    ...selectedOrderColumnClasses,
                    ...selectedExoticColumnClasses,
                    ...selectedCssGridStartClasses
                ].map((option) => option.value).filter(Boolean);

                setAttributes({ columnClasses: combinedClasses });

                // Logging for debugging
                // console.log('Selected CSS Grid Column Classes: ', selectedCssGridColumnClasses);
                // console.log('Selected Overflow Classes: ', selectedOverflowClasses);
                // console.log('Selected Padding Classes: ', selectedPaddingClasses);
                // console.log('Selected Order Column Classes: ', selectedOrderColumnClasses);
                // console.log('Selected Exotic Column Classes: ', selectedExoticColumnClasses);
                // console.log('Combined Classes: ', combinedClasses);
            }, [selectedCssGridColumnClasses, selectedOverflowClasses, selectedPaddingClasses, selectedOrderColumnClasses, selectedExoticColumnClasses, selectedCssGridStartClasses]);

            // Handler function to update state for class selectors
            const handleClassChange = (setter) => (newValue) => {
                setter(newValue || []);
            };

            // Ensure value prop passed to ReactSelect is in correct format
            const getSelectedOptions = (selectedClasses, options) => options.filter((option) => selectedClasses.map((opt) => opt.value).includes(option.value));

            return (
                <>
                    <BlockControls>
                        <ToolbarButton
                            icon="plus"
                            label={__('Add Inner Block')}
                            onClick={() => {
                                const { insertBlock } = dispatch('core/block-editor');
                                const { getBlock } = select('core/block-editor');
                                const block = getBlock(clientId);
                                const newBlock = wp.blocks.createBlock('core/paragraph', { placeholder: 'Enter paragraph text...' });
                                insertBlock(newBlock, block.innerBlocks.length, clientId);
                            }}
                        />
                    </BlockControls>
                    <InspectorControls>
                        <PanelBody title={__('Grid Column Settings')}>
                            <ReactSelect
                                isMulti
                                value={getSelectedOptions(selectedCssGridColumnClasses, cssGridColumnOptions)}
                                options={cssGridColumnOptions}
                                onChange={handleClassChange(setSelectedCssGridColumnClasses)}
                                className="react-select-container"
                                classNamePrefix="react-select"
                            />
                        </PanelBody>
                        <PanelBody title={__('Overflow Settings')}>
                            <ReactSelect
                                isMulti
                                value={getSelectedOptions(selectedOverflowClasses, overflowOptions)}
                                options={overflowOptions}
                                onChange={handleClassChange(setSelectedOverflowClasses)}
                                className="react-select-container"
                                classNamePrefix="react-select"
                                style={{ marginTop: '10px' }}
                            />
                        </PanelBody>
                        <PanelBody title={__('Padding Settings')}>
                            <ReactSelect
                                isMulti
                                value={getSelectedOptions(selectedPaddingClasses, paddingOptions)}
                                options={paddingOptions}
                                onChange={handleClassChange(setSelectedPaddingClasses)}
                                className="react-select-container"
                                classNamePrefix="react-select"
                                style={{ marginTop: '10px' }}
                            />
                        </PanelBody>
                        <PanelBody title={__('Order Settings')}>
                            <ReactSelect
                                isMulti
                                value={getSelectedOptions(selectedOrderColumnClasses, orderOptions)}
                                options={orderOptions}
                                onChange={handleClassChange(setSelectedOrderColumnClasses)}
                                className="react-select-container"
                                classNamePrefix="react-select"
                                style={{ marginTop: '10px' }}
                            />
                        </PanelBody>
                        <PanelBody title={__('CSS Grid Start Settings')}>
                            <ReactSelect
                                isMulti
                                value={getSelectedOptions(selectedCssGridStartClasses, cssGridStartOptions)}
                                options={cssGridStartOptions}
                                onChange={handleClassChange(setSelectedCssGridStartClasses)}
                                className="react-select-container"
                                classNamePrefix="react-select"
                                style={{ marginTop: '10px' }}
                            />
                        </PanelBody>
                        <PanelBody title={__('Experimental Settings')}>
                            <small>These settings are experimental and may not work as expected.</small><br/>
                            <small>This will cause the column to be flush right or left against the edge of the window when on desktop</small>
                            <ReactSelect
                                isMulti
                                value={getSelectedOptions(selectedExoticColumnClasses, exoticColumnOptions)}
                                options={exoticColumnOptions}
                                onChange={handleClassChange(setSelectedExoticColumnClasses)}
                                className="react-select-container"
                                classNamePrefix="react-select"
                                isOptionDisabled={() => getSelectedOptions(selectedExoticColumnClasses, exoticColumnOptions).length >= 1}
                                style={{ marginTop: '10px' }}
                            />
                        </PanelBody>
                        <PanelBody title={__('Background Color')}>
                            <ColorPalette
                                value={backgroundColor}
                                onChange={(color) => setAttributes({ backgroundColor: color })}
                                enableAlpha
                            />
                        </PanelBody>
                    </InspectorControls>
                    <div
                        {...blockProps}
                        ref={blockRef}
                        style={{ backgroundColor: backgroundColor || undefined }}
                        className={`g-col ${[
                            ...selectedCssGridColumnClasses,
                            ...selectedOverflowClasses,
                            ...selectedPaddingClasses,
                            ...selectedOrderColumnClasses,
                            ...selectedExoticColumnClasses,
                            ...selectedCssGridStartClasses
                        ].map((option) => option.value).join(' ')}`}
                    >
                        <InnerBlocks
                            renderAppender={false} // Disable the default appender
                            template={[
                                ['core/heading', { placeholder: 'Enter heading text...' }],
                                ['core/paragraph', { placeholder: 'Enter paragraph text...' }]
                            ]}
                        />
                    </div>
                </>
            );
        },
        save: ({ attributes }) => {
            const { columnClasses, backgroundColor } = attributes;

            return (
                <div
                    className={`g-col ${columnClasses.join(' ')}`}
                    style={{ backgroundColor: backgroundColor || undefined }}
                >
                    <InnerBlocks.Content />
                </div>
            );
        },
    });
}

/**
 * Adding a New Option
 * ===================
 * To add a new option to any of the select dropdowns, follow these steps:
 *
 * 1. Update the relevant options array:
 *    - Go to `ColumnOptions.js`.
 *    - Add your new option to the desired options array (e.g., `cssGridColumnOptions`, `overflowOptions`, etc.).
 *      Ensure each option is an object with `label` and `value` properties.
 *
 *    Example:
 *    ```jsx
 *    export const cssGridColumnOptions = [
 *        { label: '1 Column', value: 'col-1' },
 *        { label: '2 Columns', value: 'col-2' },
 *        // Add new option here
 *        { label: '3 Columns', value: 'col-3' },
 *    ];
 *    ```
 *
 * 2. Ensure the new option is handled in the component state:
 *    - No additional changes are needed in the component state handling as it dynamically updates based on the options array.
 *
 * 3. Save and test:
 *    - Save your changes and test the block in the WordPress editor.
 *    - Ensure the new option appears in the dropdown and functions correctly.
 */

In this blog post, we’ve detailed the creation of a custom Gutenberg block for Bootstrap grid columns. 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!

Also see its parents components: