Search code examples
phpwordpresswordpress-gutenberg

loop through attributes in wordpress


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>
  )
}

Result: Result


Solution

  • 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...