0

Description:

I have developed a custom block called 'Stylized List' for the WordPress block editor (Gutenberg) that allows users to create advanced styled lists with customizable titles, items, and icons. The block is designed to be reusable, and users can add multiple instances of it to their posts or pages.

Problem:

The main issue I am facing with the 'Stylized List' block is that the list items are not being rendered correctly, and the content is getting looped between different instances of the block. When I customize the list items for one 'Stylized List' block, the same items appear in all other 'Stylized List' blocks on the page.

Steps to Reproduce:

Add multiple instances of the 'Stylized List' block to a post or page. Customize the list items (titles and icons) for each block individually. Save the post or page.

Expected Behavior:

Each instance of the 'Stylized List' block should have its own unique list items, and the content should be displayed correctly as per the customization made to each block.

Actual Behavior:

The list items of one 'Stylized List' block are appearing in all other instances of the block, causing duplication and incorrect rendering of content.

Code Snippets:

Here is the relevant code for the 'Stylized List' block:

block.json:

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 2,
  "name": "lgb/stylized-list",
  "version": "1.0.0",
  "title": "Stylized List",
  "category": "lgb-custom-category",
  "description": "Create advanced styled list",
  "supports": {
    "html": false
  },
  "attributes": {
    "listHeading": {
      "type": "string"
    },
    "listItems": {
      "type": "string",
      "default": "[{\"title\":\"\",\"icon\":\"\"}]"
    },
    "listCaption": {
      "type": "string"
    }
  },
  "textdomain": "lendeducom",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css"
}

edit.js:

import { __ } from "@wordpress/i18n";
import {
    useBlockProps,
    RichText,
    BlockControls,
} from "@wordpress/block-editor";
import { useState } from "@wordpress/element";
import List from "./components/list";
import SidebarSettings from "./components/sidebarsettings";
import "./css/editor.scss";

export default function Edit(props) {

    const {
        attributes: { listHeading, listItems, listCaption },
        setAttributes,
    } = props;
    const blockProps = useBlockProps();
    const parseListItems = JSON.parse(listItems);
    const [iconIndex, setIconIndex ]= useState();
    console.log({listItems});
    return (
        <div {...blockProps}>
            <SidebarSettings
                listHeading={listHeading}
                setAttributes={setAttributes}
                listCaption={listCaption}
            />
            <section className="stylized-list-container">
                <div className="lists">
                    <div className="lists-title">
                        <p className="heading-6">
                            {listHeading ? listHeading : "Title placeholder"}
                        </p>
                    </div>
                    <ul>
                        {parseListItems.map((item, index) => {
                            return (
                                <List
                                    index={index}
                                    item={item}
                                    key={index}
                                    onChangeTitle={(title, index) => {
                                        parseListItems[index].title = title;
                                        setAttributes({
                                            listItems: JSON.stringify(parseListItems),
                                        });
                                    }}
                                    removeItem={parseListItems}
                                    setAttributes={setAttributes}
                                    listItems={parseListItems}
                                />
                            );
                        })}
                    </ul>
                    <button
                        className="addNewItem"
                        onClick={() => {
                            parseListItems.push({
                                title: "",
                            });
                            setAttributes({ listItems: JSON.stringify(parseListItems) });
                        }}
                    >
                        Add New Item
                    </button>
                </div>
                <RichText
                    tagName="p"
                    className="list-caption"
                    value={listCaption}
                    onChange={(val) => setAttributes({ listCaption: val })}
                    placeholder="Add caption here"
                    allowedFormats={[
                        "core/bold",
                        "core/italic",
                        "core/link",
                        "core/superscript",
                        "lgb/product-update",
                    ]}
                />
            </section>
        </div>
    );
}

list.js:

import { __ } from "@wordpress/i18n";
import {
    RichText,
} from "@wordpress/block-editor";
import { Popover, Button } from "@wordpress/components";
import { useState } from "@wordpress/element";
import yesIcon from "./../../assets/img/icon-general-yes.svg";
import noIcon from "./../../assets/img/icon-general-no.svg";
import noGrayIcon from "./../../assets/img/icon-general-no-gray.svg";

const List = (props) => {
    const {
            index,
            item,
            onChangeTitle,
            removeItem,
            setAttributes,
            listItems,
        } = props;
    function conditionalListIcon(icon) {
        switch (icon) {
            case "yes":
                return <img src={yesIcon} />;
            case "no":
                return <img src={noIcon} />;
            case "nogray":
                return <img src={noGrayIcon} />;
            default:
                return <img src={yesIcon} />;
        }
    }
    const [isVisible, setIsVisible] = useState(false);
    const toggleVisible = () => {
        setIsVisible((state) => !state);
    };
    return (
        <li>
            {/* <div className="list-with-icon"> */}
            {conditionalListIcon(item.icon)}
            <RichText
                tagName="p"
                className="list"
                value={item.title}
                allowedFormats={[
                    "core/bold",
                    "core/italic",
                    "core/link",
                    "core/superscript",
                    "lgb/product-update",
                ]}
                onChange={(content) => onChangeTitle(content, index)}
                placeholder={__("Add List Item")}
                onReplace={() => {}}
                onSplit={() => {}}
                multiline={false}
            />
            {/* </div> */}
            <Button
                variant="secondary"
                className="list-icon-toggle"
                onClick={toggleVisible}
            >
                Icons
                {isVisible && (
                    <ul className="icons">
                        <li
                            onClick={() => {
                                listItems[index].icon = "yes";
                                setAttributes({ listItems: JSON.stringify([...listItems]) });
                            }}
                        >
                            <img src={yesIcon} />
                        </li>
                        <li
                            onClick={() => {
                                listItems[index].icon = "no";
                                setAttributes({ listItems: JSON.stringify([...listItems]) });
                                console.log(listItems[index]);
                            }}
                        >
                            <img src={noIcon} />
                        </li>
                        <li
                            onClick={() => {
                                listItems[index].icon = "nogray";
                                setAttributes({ listItems: JSON.stringify([...listItems]) });
                                console.log(listItems[index]);
                            }}
                        >
                            <img src={noGrayIcon} />
                        </li>
                    </ul>
                )}
            </Button>
            <button
                className="removeItem"
                onClick={() => {
                    removeItem.splice(index, 1);
                    setAttributes({ listItems: JSON.stringify(removeItem) });
                }}
            >
                x
            </button>
        </li>
    );
};

export default List;

console.log of attributes:

{listItems: '[{"title":"item one[1]","icon":""},{"title":"item two[1]","icon":"yes"},{"title":"Item three[1]"}]'}
{listItems: '[{"title":"item one [two]","icon":""},{"title":"Item two[two]"},{"title":"Item three[two]"}]'}

Screenshots: enter image description here

Expected Solution: I am seeking assistance from the WordPress community to help identify the root cause of this issue and provide a solution to ensure that each instance of the 'Stylized List' block maintains its own unique list items without interfering with other instances. I appreciate any insights or suggestions to resolve this problem and improve the functionality of the 'Stylized List' block. Thank you in advance for your help!

7
  • there are several possible things that can't be ruled out because you hid parts of your code to try and be concise, e.g. it's not possible to tell if you used keys or not on the components created in loops/maps to help React, or if you used global variables or variables outside of your components. I do see a major antipattern though, instead of using innerblocks you've used attributes to store the list items. Even the standard <ul> block doesn't do this in modern WP and uses a list block with a list item block and controls on allowed child/parent blocks. That approach is a lot easier
    – Tom J Nowell
    Jul 21 at 12:25
  • @TomJNowell See how values are being stored using Gutenberg attributes: ``` export default function Edit(props) { const { attributes: { listHeading, listItems, listCaption }, setAttributes, } = props; const blockProps = useBlockProps(); const parseListItems = JSON.parse(listItems); const [iconIndex, setIconIndex ]= useState(); console.log({listItems}); ``` Jul 21 at 12:48
  • 1
    comments don't support multiline code blocks, and not everybody reads them, use the edit link to update and improve your question. It's also clear you're using an attribute to store an object full of list items from the block definition and dumped attributes, but it would be enormously helpful if you replaced a lot of the comments with the actual code, you've hidden too much of it to answer the question. Also your screenshot doesn't show the problem, items in list one have [1] at the end, but items in list 2 have [two], they are not the same
    – Tom J Nowell
    Jul 21 at 12:50
  • @TomJNowell in the screenshot another block item list rendering in the first block. Also, I have updated my code. Jul 21 at 13:03
  • I'm not seeing key attributes in your updated code, you should have a look at react.dev/learn/rendering-lists
    – Tom J Nowell
    Jul 21 at 13:11

1 Answer 1

0

Use unique indices for List item keys. I.e., use package like https://www.npmjs.com/package/uuid to generate unique indices or useInstanceId to generate them. Note that if you use useInstanceId generated Id's will be unique, but you will get odd or even numbers because of the way React renders components (which does not matter, unless you want to reference those Id's in your code for whatever reason, what you typically don't do)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.