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: