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: