For the dynamic block version of this code, please see this GitHub link.
In this blog post, we will explore how to create a child Gutenberg block for Bootstrap rows. This block complements the previously created Bootstrap container block by allowing users to add and configure rows within the container. Using react-select for a rich user interface, this block provides flexibility in layout and alignment options. Let’s break down the code step-by-step. Please see the previous article for how to create the parent container block.
Importing Dependencies
// Importing the necessary dependencies and options
import ReactSelect from 'react-select'; // Importing ReactSelect for multi-select dropdowns
import { rowOptions, alignItemsOptions, justifyContentOptions } from '../columns/ColumnOptions'; // Importing options for row, alignment, and justify content classes
// those import variables are arrays of objects rowOptions= [ { label: 'row-cols-1', value:'row-cols-1'}, 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 import react-select for the multi-select dropdown component and the rowOptions, alignItemsOptions, and justifyContentOptions 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 gutenbergBlocksBootstrapRow(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, Button } = 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 row block. We extract necessary WordPress and React utilities from the wp object passed to this function.
Edit Function
The edit function is essential 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, clientId }) => {
const { rowClass = [] } = attributes; // Destructuring attributes
// Setting up state hooks for managing selected classes
const [selectedRowClasses, setSelectedRowClasses] = useState(
rowClass.filter((value) => rowOptions.find((option) => option.value === value)).map((value) => rowOptions.find((option) => option.value === value))
);
const [selectedAlignItemsClasses, setSelectedAlignItemsClasses] = useState(
rowClass.filter((value) => alignItemsOptions.find((option) => option.value === value)).map((value) => alignItemsOptions.find((option) => option.value === value))
);
const [selectedJustifyContentClasses, setSelectedJustifyContentClasses] = useState(
rowClass.filter((value) => justifyContentOptions.find((option) => option.value === value)).map((value) => justifyContentOptions.find((option) => option.value === value))
);
We use the useState hook to manage the state of selected row, alignment, and justify content 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(() => {
const combinedClasses = [
...selectedRowClasses,
...selectedAlignItemsClasses,
...selectedJustifyContentClasses
].filter((option) => option).map((option) => option.value);
setAttributes({
rowClass: combinedClasses
});
const blockEditor = document.querySelector(`.block-editor-block-list__block[data-block="${clientId}"] > .row > .block-editor-inner-blocks > .block-editor-block-list__layout`);
if (blockEditor) {
blockEditor.className = `block-editor-block-list__layout row ${combinedClasses.join(' ')}`;
}
}, [selectedRowClasses, selectedAlignItemsClasses, selectedJustifyContentClasses]);
The useEffect hooks ensure that whenever the selected row, alignment, or justify content classes change, the block’s attributes are updated accordingly. This also updates the block’s class names in the editor dynamically.
// Handlers for when selection changes
const handleRowClassChange = (newValue) => {
setSelectedRowClasses(newValue || []);
};
const handleAlignItemsClassChange = (newValue) => {
setSelectedAlignItemsClasses(newValue || []);
};
const handleJustifyContentClassChange = (newValue) => {
setSelectedJustifyContentClasses(newValue || []);
};
These handler functions update the state when the user selects new options from the dropdowns.
// Functions to add and remove columns
const addColumn = () => {
wp.data.dispatch('core/block-editor').insertBlocks(
wp.blocks.createBlock('bs5/bootstrap-column'),
wp.data.select('core/block-editor').getBlock(clientId).innerBlocks.length,
clientId
);
};
const removeColumn = () => {
const { innerBlocks } = wp.data.select('core/block-editor').getBlock(clientId);
if (innerBlocks.length > 0) {
const lastBlockClientId = innerBlocks[innerBlocks.length - 1].clientId;
wp.data.dispatch('core/block-editor').removeBlock(lastBlockClientId);
}
};
These functions handle adding and removing columns within the row block. The addColumn function inserts a new column block, while the removeColumn function removes the last column block.
return (
<Fragment>
<InspectorControls>
<PanelBody title={__('Row Settings')}>
<ReactSelect
isMulti
value={selectedRowClasses}
options={rowOptions}
onChange={handleRowClassChange}
className="react-select-container"
classNamePrefix="react-select"
/>
</PanelBody>
<PanelBody title={__('Alignment Settings')}>
<ReactSelect
isMulti
value={selectedAlignItemsClasses}
options={alignItemsOptions}
onChange={handleAlignItemsClassChange}
className="react-select-container"
classNamePrefix="react-select"
/>
</PanelBody>
<PanelBody title={__('Justify Settings')}>
<ReactSelect
isMulti
value={selectedJustifyContentClasses}
options={justifyContentOptions}
onChange={handleJustifyContentClassChange}
className="react-select-container"
classNamePrefix="react-select"
/>
</PanelBody>
</InspectorControls>
<div className={`row ${rowClass.join(' ')}`}>
<InnerBlocks
allowedBlocks={['bs5/bootstrap-column']}
template={[['bs5/bootstrap-column']]}
renderAppender={InnerBlocks.ButtonBlockAppender}
/>
</div>
<div className="button-group">
<Button onClick={addColumn} className="btn btn-warning" style={{ marginTop: '10px' }}>
{__('Add Column')}
</Button>
<Button onClick={removeColumn} className="btn btn-danger" style={{ marginTop: '10px', marginLeft: '10px' }}>
{__('Remove Column')}
</Button>
</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 row, alignment, and justify content classes. The InnerBlocks component allows users to add child blocks within our custom row block. The buttons at the bottom allow users to add or remove columns dynamically.
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 { rowClass } = attributes; // Destructuring attributes
return (
<div className={`row ${rowClass.join(' ')}`}>
<InnerBlocks.Content />
</div>
);
};
In the save function, we simply render a div element with the selected row 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-row', {
title: __('Bootstrap Row'), // Block title
icon: 'layout', // Block icon
category: 'layout', // Block category
attributes: {
rowClass: {
type: 'array',
default: [], // Default row 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 row block:
import ReactSelect from 'react-select';
import { rowOptions, alignItemsOptions, justifyContentOptions } from '../columns/ColumnOptions';
export default function gutenbergBlocksBootstrapRow(wp) {
const { registerBlockType } = wp.blocks;
const { __ } = wp.i18n;
const { InspectorControls, InnerBlocks } = wp.blockEditor;
const { PanelBody, Button } = wp.components;
const { useState, useEffect, Fragment } = wp.element;
const edit = ({ attributes, setAttributes, clientId }) => {
const { rowClass = [] } = attributes;
const [selectedRowClasses, setSelectedRowClasses] = useState(
rowClass.filter((value) => rowOptions.find((option) => option.value === value)).map((value) => rowOptions.find((option) => option.value === value))
);
const [selectedAlignItemsClasses, setSelectedAlignItemsClasses] = useState(
rowClass.filter((value) => alignItemsOptions.find((option) => option.value === value)).map((value) => alignItemsOptions.find((option) => option.value === value))
);
const [selectedJustifyContentClasses, setSelectedJustifyContentClasses] = useState(
rowClass.filter((value) => justifyContentOptions.find((option) => option.value === value)).map((value) => justifyContentOptions.find((option) => option.value === value))
);
useEffect(() => {
const combinedClasses = [
...selectedRowClasses,
...selectedAlignItemsClasses,
...selectedJustifyContentClasses
].filter((option) => option).map((option) => option.value);
setAttributes({
rowClass: combinedClasses
});
const blockEditor = document.querySelector(`.block-editor-block-list__block[data-block="${clientId}"] > .row > .block-editor-inner-blocks > .block-editor-block-list__layout`);
if (blockEditor) {
blockEditor.className = `block-editor-block-list__layout row ${combinedClasses.join(' ')}`;
}
}, [selectedRowClasses, selectedAlignItemsClasses, selectedJustifyContentClasses]);
const handleRowClassChange = (newValue) => {
setSelectedRowClasses(newValue || []);
};
const handleAlignItemsClassChange = (newValue) => {
setSelectedAlignItemsClasses(newValue || []);
};
const handleJustifyContentClassChange = (newValue) => {
setSelectedJustifyContentClasses(newValue || []);
};
const addColumn = () => {
wp.data.dispatch('core/block-editor').insertBlocks(
wp.blocks.createBlock('bs5/bootstrap-column'),
wp.data.select('core/block-editor').getBlock(clientId).innerBlocks.length,
clientId
);
};
const removeColumn = () => {
const { innerBlocks } = wp.data.select('core/block-editor').getBlock(clientId);
if (innerBlocks.length > 0) {
const lastBlockClientId = innerBlocks[innerBlocks.length - 1].clientId;
wp.data.dispatch('core/block-editor').removeBlock(lastBlockClientId);
}
};
return (
<Fragment>
<InspectorControls>
<PanelBody title={__('Row Settings')}>
<ReactSelect
isMulti
value={selectedRowClasses}
options={rowOptions}
onChange={handleRowClassChange}
className="react-select-container"
classNamePrefix="react-select"
/>
</PanelBody>
<PanelBody title={__('Alignment Settings')}>
<ReactSelect
isMulti
value={selectedAlignItemsClasses}
options={alignItemsOptions}
onChange={handleAlignItemsClassChange}
className="react-select-container"
classNamePrefix="react-select"
/>
</PanelBody>
<PanelBody title={__('Justify Settings')}>
<ReactSelect
isMulti
value={selectedJustifyContentClasses}
options={justifyContentOptions}
onChange={handleJustifyContentClassChange}
className="react-select-container"
classNamePrefix="react-select"
/>
</PanelBody>
</InspectorControls>
<div className={`row ${rowClass.join(' ')}`}>
<InnerBlocks
allowedBlocks={['bs5/bootstrap-column']}
template={[['bs5/bootstrap-column']]}
renderAppender={InnerBlocks.ButtonBlockAppender}
/>
</div>
<div className="button-group">
<Button onClick={addColumn} className="btn btn-warning" style={{ marginTop: '10px' }}>
{__('Add Column')}
</Button>
<Button onClick={removeColumn} className="btn btn-danger" style={{ marginTop: '10px', marginLeft: '10px' }}>
{__('Remove Column')}
</Button>
</div>
</Fragment>
);
};
const save = ({ attributes }) => {
const { rowClass } = attributes;
return (
<div className={`row ${rowClass.join(' ')}`}>
<InnerBlocks.Content />
</div>
);
};
registerBlockType('bs5/bootstrap-row', {
title: __('Bootstrap Row'),
icon: 'layout',
category: 'layout',
attributes: {
rowClass: {
type: 'array',
default: [],
}
},
edit,
save,
});
}
In this blog post, we’ve detailed the creation of a custom Gutenberg block for Bootstrap rows. 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!