woracious-suite/ ├── blocks/ │ └── wo-custom-sidebar-menu/ │ ├── block.json │ ├── edit.js │ ├── editor.scss │ ├── index.js │ ├── render.php │ ├── style.scss │ └── view.js ├── build/ │ ├── index.asset.php │ ├── index.js │ ├── style-index-rtl.css │ └── style-index.css ├── example-static.php ├── package-lock.json ├── package.json └── readme.txt
Above is the file structure. Prveiously I had working block in Monolithoc style that I wanted to convert into what is prescribed by WordPress.
block.json —
{
"$schema": ".json",
"apiVersion": 2,
"name": "woracioussuite/cust-sidebar-menu",
"version": "1.0.0",
"title": "Woracious Custom Sidebar Menu",
"category": "theme",
"icon": "menu",
"description": "A custom sidebar menu block that can display custom menu items or categories.",
"supports": {
"html": false
},
"attributes": {
"heading": {
"type": "string",
"default": "Header"
},
"menuItems": {
"type": "array",
"default": []
},
"useCategories": {
"type": "boolean",
"default": false
},
"categoryOrderBy": {
"type": "string",
"default": "name"
},
"categoryOrder": {
"type": "string",
"default": "ASC"
},
"hideEmpty": {
"type": "boolean",
"default": true
},
"selectedCategories": {
"type": "array",
"default": []
},
"useSelectedCategoriesOnly": {
"type": "boolean",
"default": false
}
},
"textdomain": "woracious-suite",
"editorScript": "file:./index.js",
"render": "file:./render.php"
}
package.json —
{
"name": "woracious-suite",
"version": "0.1.0",
"description": "Example block scaffolded with Create Block tool.",
"author": "James Bond 007",
"license": "GPL-2.0-or-later",
"main": "build/index.js",
"scripts": {
"start": "wp-scripts start blocks/wo-custom-sidebar-menu/index.js",
"build": "wp-scripts build --blocks-manifest",
"format": "wp-scripts format",
"lint:css": "wp-scripts lint-style",
"lint:js": "wp-scripts lint-js",
"packages-update": "wp-scripts packages-update",
"plugin-zip": "wp-scripts plugin-zip"
},
"devDependencies": {
"@wordpress/scripts": "^30.15.0"
}
}
index.js—
/**
* Registers a new block provided a unique name and an object defining its behavior.
*
* @see /
*/
import { registerBlockType } from "@wordpress/blocks";
/**
* Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
* All files containing `style` keyword are bundled together. The code used
* gets applied both to the front of your site and to the editor.
*
* @see /@wordpress/scripts#using-css
*/
import "./style.scss";
/**
* Internal dependencies
*/
import Edit from "./edit";
import metadata from "./block.json";
/**
* Every block starts by registering a new block type definition.
*
* @see /
*/
registerBlockType(metadata.name, {
...metadata,
edit: Edit,
// No save function as we're using server-side rendering
save: () => null,
});
edit.js:
/**
* WordPress dependencies
*/
import { __ } from "@wordpress/i18n";
import { InspectorControls, useBlockProps } from "@wordpress/block-editor";
import {
Button,
CheckboxControl,
PanelBody,
SelectControl,
TextControl,
ToggleControl,
} from "@wordpress/components";
import { useEffect, useState } from "@wordpress/element";
import apiFetch from "@wordpress/api-fetch";
/**
* Edit component for the custom sidebar menu block
*
* @param {Object} props
* @returns {JSX.Element} The edit component
*/
export default function Edit({ attributes, setAttributes }) {
const {
heading,
menuItems,
useCategories,
categoryOrderBy,
categoryOrder,
hideEmpty,
selectedCategories,
useSelectedCategoriesOnly,
} = attributes;
// State for storing fetched categories
const [categories, setCategories] = useState([]);
const [isLoading, setIsLoading] = useState(false);
// Fetch categories when component mounts or when ordering options change
useEffect(() => {
if (useCategories) {
setIsLoading(true);
const queryParams =
`?per_page=100&orderby=${categoryOrderBy}&order=${categoryOrder}` +
(hideEmpty ? "&hide_empty=true" : "");
apiFetch({ path: "/wp/v2/categories" + queryParams })
.then((fetchedCategories) => {
setCategories(fetchedCategories);
setIsLoading(false);
})
.catch((error) => {
console.error("Error fetching categories:", error);
setIsLoading(false);
});
}
}, [useCategories, categoryOrderBy, categoryOrder, hideEmpty]);
// Function to add a new menu item
function addMenuItem() {
const newItems = [...menuItems];
newItems.push({ text: "", url: "" });
setAttributes({ menuItems: newItems });
}
// Function to update a menu item
function updateMenuItem(index, field, value) {
const newItems = [...menuItems];
newItems[index][field] = value;
setAttributes({ menuItems: newItems });
}
// Function to remove a menu item
function removeMenuItem(index) {
const newItems = [...menuItems];
newItems.splice(index, 1);
setAttributes({ menuItems: newItems });
}
// Function to toggle category selection
function toggleCategorySelection(categoryId) {
const newSelection = [...selectedCategories];
const index = newSelection.indexOf(categoryId);
if (index === -1) {
newSelection.push(categoryId);
} else {
newSelection.splice(index, 1);
}
setAttributes({ selectedCategories: newSelection });
}
// Prepare preview items
let previewItems = [];
if (useCategories) {
if (isLoading) {
previewItems.push(<li key="loading">Loading categories...</li>);
} else if (categories.length === 0) {
previewItems.push(<li key="none">No categories found</li>);
} else {
let displayCategories = categories;
if (useSelectedCategoriesOnly && selectedCategories.length > 0) {
displayCategories = categories.filter((category) =>
selectedCategories.includes(category.id),
);
}
previewItems = displayCategories.slice(0, 5).map((category) => (
<li key={category.id}>
<a href="#">
{category.name} ({category.count})
</a>
</li>
));
if (displayCategories.length > 5) {
previewItems.push(<li key="more">...</li>);
}
}
} else {
if (menuItems.length === 0) {
previewItems.push(<li key="add">Add menu items in the sidebar</li>);
} else {
previewItems = menuItems.map((item, index) => (
<li key={index}>
<a href={item.url || "#"}>{item.text || "Menu Item"}</a>
</li>
));
}
}
return (
<>
<InspectorControls>
<PanelBody
title={__("Menu Settings", "neatminimum2025")}
initialOpen={true}
>
<TextControl
label={__("Heading", "neatminimum2025")}
value={heading}
onChange={(value) => setAttributes({ heading: value })}
/>
<ToggleControl
label={__("Use Categories", "neatminimum2025")}
checked={useCategories}
onChange={(value) => setAttributes({ useCategories: value })}
/>
{!useCategories && (
<div className="menu-items-controls">
<h3>{__("Menu Items", "neatminimum2025")}</h3>
{menuItems.map((item, index) => (
<div className="menu-item" key={index}>
<TextControl
label={__("Text", "neatminimum2025")}
value={item.text}
onChange={(value) => updateMenuItem(index, "text", value)}
/>
<TextControl
label={__("URL", "neatminimum2025")}
value={item.url}
onChange={(value) => updateMenuItem(index, "url", value)}
/>
<Button
isDestructive={true}
onClick={() => removeMenuItem(index)}
>
{__("Remove", "neatminimum2025")}
</Button>
</div>
))}
<Button isPrimary={true} onClick={addMenuItem}>
{__("Add Menu Item", "neatminimum2025")}
</Button>
</div>
)}
{useCategories && (
<PanelBody
title={__("Category Settings", "neatminimum2025")}
initialOpen={true}
>
<SelectControl
label={__("Order By", "neatminimum2025")}
value={categoryOrderBy}
options={[
{ label: __("Name", "neatminimum2025"), value: "name" },
{ label: __("Count", "neatminimum2025"), value: "count" },
{ label: __("ID", "neatminimum2025"), value: "id" },
]}
onChange={(value) => setAttributes({ categoryOrderBy: value })}
/>
<SelectControl
label={__("Order", "neatminimum2025")}
value={categoryOrder}
options={[
{ label: __("Ascending", "neatminimum2025"), value: "asc" },
{ label: __("Descending", "neatminimum2025"), value: "desc" },
]}
onChange={(value) => setAttributes({ categoryOrder: value })}
/>
<ToggleControl
label={__("Hide Empty Categories", "neatminimum2025")}
checked={hideEmpty}
onChange={(value) => setAttributes({ hideEmpty: value })}
/>
<ToggleControl
label={__("Show Only Selected Categories", "neatminimum2025")}
checked={useSelectedCategoriesOnly}
onChange={(value) =>
setAttributes({ useSelectedCategoriesOnly: value })
}
/>
{useSelectedCategoriesOnly && categories.length > 0 && (
<div className="category-selection">
<h4>{__("Select Categories", "neatminimum2025")}</h4>
{categories.map((category) => (
<CheckboxControl
key={category.id}
label={`${category.name} (${category.count})`}
checked={selectedCategories.includes(category.id)}
onChange={() => toggleCategorySelection(category.id)}
/>
))}
</div>
)}
{isLoading && (
<p>{__("Loading categories...", "neatminimum2025")}</p>
)}
</PanelBody>
)}
</PanelBody>
</InspectorControls>
<div {...useBlockProps({ className: "menu" })}>
<h6>{heading}</h6>
<ul>{previewItems}</ul>
</div>
</>
);
}
example-static.php —
<?php
/**
* Plugin Name: Woracious Block Suite
* Description: Example block scaffolded with Create Block tool.
* Version: 0.1.0
* Requires at least: 6.7
* Requires PHP: 7.4
* Author: The WordPress Contributors
* License: GPL-2.0-or-later
* License URI: .0.html
* Text Domain: woracious-suite
*
* @package Woracioussuite
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Register custom blocks
*/
function woracious_suite_register_custom_blocks() {
register_block_type( __DIR__ . '/blocks/wo-custom-sidebar-menu' );
}
add_action( 'init', 'woracious_suite_register_custom_blocks' );