I made a menu with custom block in wordpress gutenberg and made it editable using RichText component but every time I try to edit those, changes will apply to all of them, instead I want individual texts. I know it can be done by looping through objects but I don't know how
Sidebar block:
import { RichText } from "@wordpress/block-editor"
import { registerBlockType } from "@wordpress/blocks"
registerBlockType("ourblocktheme/sidebarmenucontent", {
attributes: {
text: {type: 'string'},
size: {type: 'string'}
},
title: "Sidebar Menu Content",
edit: EditComponent,
save: SaveComponent
})
function EditComponent(props) {
function textHandler(x) {
props.setAttributes({ text: x })
}
return (
<>
<ul className="main-menu">
<li>
<a data-scroll-nav="0" href="#home">
<span className="m-icon">
<i className="bi-house-door"></i>
</span>
<RichText value={props.attributes.text} onChange={textHandler} />
</a>
</li>
<li>
<a data-scroll-nav="1" href="#services">
<span className="m-icon">
<i className="bi-person"></i>
</span>
<RichText value={props.attributes.text} onChange={textHandler} />
</a>
</li>
<li>
<a data-scroll-nav="2" href="#services">
<span className="m-icon">
<i className="bi-briefcase"></i>
</span>
<RichText value={props.attributes.text} onChange={textHandler} />
</a>
</li>
<li>
<a data-scroll-nav="3" href="#work">
<span className="m-icon">
<i className="bi-columns"></i>
</span>
<RichText value={props.attributes.text} onChange={textHandler} />
</a>
</li>
<li>
<a data-scroll-nav="4" href="#contactus">
<span className="m-icon">
<i className="bi-telephone"></i>
</span>
<RichText value={props.attributes.text} onChange={textHandler} />
</a>
</li>
</ul>
</>
)
}
function SaveComponent() {
return (
<div>Hello</div>
)
}
In your block attributes, an array of strings is needed to enable storing multiple text strings for your menu items. Looking at the structure of <ul><li><a><span><i>..</i></a></li><ul>
, an attribute query can extract the values from the markup into a useful array, eg:
attributes: {
menuItems: {
type: 'array',
source: 'query', // Searches the markup
selector: 'li', // for each <li> element
query: {
text: {
type: 'string',
selector: 'span.label', // then find this selector
source: 'text' // get value of text
},
href: {
type: 'string',
source: 'attribute', // and this attributes
selector: 'a[href]' // value of link href
},
className: {
type: 'string',
source: 'attribute', // and this attributes
selector: 'i[class]' // value of class
}
}
}
}
An additional <span className="label">{text}</span>
inside <a>...</a>
is required to make the query work as expected. If we queried the text of <a>
as in the current markup, it would include all the <span><i>...</i></span>
content as part of the string (which shouldn't be edited as part of the <RichText>
component).
The resulting array structure can then be used as the "default": []
value for the menuItems
attribute:
[
{
text: 'Home',
href: '#home',
className: 'bi-house-door'
},
{
text: 'Services',
href: '#services',
className: 'bi-person'
},
... // etc..
]
The next step as anticipated, is to do a "loop" in edit()
to change each text value independently. I prefer map() rather than a loop as I find more compact/cleaner and includes a useful index, eg:
edit()
function EditComponent({attributes, setAttributes}) {
const { menuItems } = attributes;
// TODO: Add updateMenuItem function defintion here..
return (
<ul className="main-menu">
{menuItems && menuItems.map(({ text, href, className }, index) => {
// If there are menuItems, map them out
return (
<li key={index}>
<a href={href}>
<span className="m-icon">
<i className={className}></i>
</span>
<RichText
value={text}
tagName="span" // tagName and className needed for query
className="label" onChange={} // TODO: Call updateMenuItem() onChange
/>
</a>
</li>
)
})}
</ul>
);
}
Unfortunately, setAttributes()
doesn't work for an array of objects, so a helper function is required to avoid mutations on an existing object, eg:
...
function updateMenuItem(text, href, className, index) {
const updatedMenuItems = [...menuItems]; // Copy of existing menuItems array
updatedMenuItems[index] = { text: text, href: href, className: className }; // Update the targeted index with new values
setAttributes({ menuItems: updatedMenuItems }); // Update menuItems with the new array
}
...
<RichText
...
onChange={(text) => updateMenuItem(text, href, className, index)}
/>
...
The final step is to ensure the content is saved to markup correctly by using the same map technique as in edit()
- with a minor change to <RichText.Content>
needed:
save()
function SaveComponent({attributes}) {
const { menuItems } = attributes;
return (
<ul>
{menuItems && menuItems.map(({ text, href, className }, index) => {
return (
<li key={index}>
<a data-scroll-nav={index} href={href}>
<span className="m-icon">
<i className={className}></i>
</span>
<RichText.Content tagName="span" className="label" value={text} />
</a>
</li>
)
})}
</ul>
);
}
If you have reached this far, hopefully you have a working, editable example based on your menu. As with everything, there are many ways to do something and while the above "works", I would suggest more block-like approach in general:
If you break your menu down, it is multiple links styled as buttons. The [core/button]
blocks is very close to that, as is [core/list]
, so I would either extend those blocks, restyle them or make your own that is just the "menu item" link/button content. I would create a parent "menu" block and use InnerBlocks to be able to add and edit as many "menu items" as you need, this would eliminate the need for query attribute and looping plus you could easily reorder them.
I find it helpful to reflect on a issue/solution, consider why it works/was challenging and then find an easier way...