Using the ESNEXT, Gutenberg provides a way to incorporate repeater blocks that

If you're like me, trying to create your own custom Gutenberg repeater block with a text and link input field can be quite challenging. I've spent hours looking at ES5 examples like this and this, but still feel stuck.

I'm reaching out for some guidance in the right direction as I could really use some help.

Here's the code I have so far, but I'm not sure where to begin with the conversion from ES5 to ESNEXT.

Edit: Forgot to mention that I aim to do this without using ACF.

// Importing necessary libraries for this block
import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
import { RichText, MediaUpload, InspectorControls } from '@wordpress/block-editor';
import { Button, ColorPicker, ColorPalette, Panel, PanelBody, PanelRow } from '@wordpress/components';

registerBlockType('ccm-block/banner-block', {
    title: __('Banner Block'),
    icon: 'format-image', // taken from Dashicons → https://developer.wordpress.org/resource/dashicons/.
    category: 'layout', // e.g. common, formatting, layout widgets, embed.
    keywords: [
        __('Banner Block'),
        __('CCM Blocks'),
    ],
    attributes: {
        mediaID: {
            type: 'number'
        },
        mediaURL: {
            type: 'string'
        },
        title: {
            type: 'array',
            source: 'children',
            selector: 'h1'
        },
        content: {
            type: 'array',
            source: 'children',
            selector: 'p'
        },
        bannerButtons: {
            type: 'array',
            source: 'children',
            selector: '.banner-buttons',
        },
        items: {
            type: 'array',
            default: []
        }
    },

    /**
     * The edit function defines the structure of the block when viewed in the editor.
     *
     * @link https://wordpress.org/gutenberg/handbook/block-api/block-edit-save/
     *
     * @param {Object} props Props.
     * @returns {JSXComponent} JSX Component.
     */
    edit: (props) => {

        const {
            attributes: { mediaID, mediaURL, title, content, bannerButtons },
            setAttributes, className
        } = props;

        const onSelectImage = (media) => {
            setAttributes({
                mediaURL: media.url,
                mediaID: media.id,
            });
        };

        const onChangeTitle = (value) => {
            setAttributes({ title: value });
        };

        const onChangeContent = (value) => {
            setAttributes({ content: value });
        };

        const onChangeBannerButtons = (value) => {
            setAttributes({ bannerButtons: value });
        };
        
        return (
            <div className={className}>
                <div id="#home-banner">
                    {/* Content goes here */}
                </div>
            </div>
        );
    },

    /**
     * The save function determines how attributes are combined into the final markup for display.
     *
     * @link https://wordpress.org/gutenberg/handbook/block-api/block-edit-save/
     *
     * @param {Object} props Props.
     * @returns {JSXFrontendHTML} JSX Frontend HTML.
     */
    save: (props) => {
        return (
            <div className={ props.className }>
                <div id="home-banner" style={{backgroundImage: `url(${ props.attributes.mediaURL })`}}>
                    {/* Saved content rendering */}
                </div>
            </div>
        );
    },
});

Edit 2: Here's my attempt at it

// Import necessary libraries for this block
import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
import { RichText, MediaUpload, InspectorControls } from '@wordpress/block-editor';
import { Button, ColorPicker, ColorPalette, Panel, PanelBody, PanelRow } from '@wordpress/components';

/**
 * Register the Custom Block
 * 
 * @link https://wordpress.org/gutenberg/handbook/block-api/
 * @param  {string}   name     name.
 * @param  {Object}   settings settings.
 * @return {?WPBlock}          The block or undefined.
 */
registerBlockType('ccm-block/banner-block', {
    title: __('Banner Block'),
    icon: 'format-image', // from Dashicons → https://developer.wordpress.org/resource/dashicons/.
    category: 'layout', // e.g. common, formatting, layout widgets, embed.
    keywords: [
        __('Banner Block'),
        __('CCM Blocks'),
    ],
    attributes: {
        mediaID: {
            type: 'number'
        },
        mediaURL: {
            type: 'string'
        },
        title: {
            type: 'array',
            source: 'children',
            selector: 'h1'
        },
        content: {
            type: 'array',
            source: 'children',
            selector: 'p'
        },
        bannerButtons: {
            type: 'array',
            source: 'children',
            selector: '.banner-buttons',
        },
        items: {
            source: 'query',
            default: [],
            selector: '.item',
            query: {
                title: {
                    type: 'string',
                    source: 'text',
                    selector: '.title'
                },
                index: {            
                    type: 'number',
                    source: 'attribute',
                    attribute: 'data-index'            
                }           
            }
        }
    },

    /**
     * The edit function defines the structure of the block when viewed in the editor.
     *
     * @link https://wordpress.org/gutenberg/handbook/block-api/block-edit-save/
     *
     * @param {Object} props Props.
     * @returns {JSXComponent} JSX Component.
     */
    edit: (props) => {

        const {
            attributes: { mediaID, mediaURL, title, content, bannerButtons, items },
            setAttributes, className
        } = props;

        const onSelectImage = (media) => {
            setAttributes({
                mediaURL: media.url,
                mediaID: media.id,
            });
        };

        const onChangeTitle = (value) => {
            setAttributes({ title: value });
        };

        const onChangeContent = (value) => {
            setAttributes({ content: value });
        };

        const onChangeBannerButtons = (value) => {
            setAttributes({ bannerButtons: value });
        };

        return (
            <div className={className}>
                <div id="#home-banner">
                    {/* Include your editing functionality here */}
                </div>
            </div>
        );
    },

    /**
     * The save function determines the output markup based on the saved attributes.
     *
     * @link https://wordpress.org/gutenberg/handbook/block-api/block-edit-save/
     *
     * @param {Object} props Props.
     * @returns {JSXFrontendHTML} JSX Frontend HTML.
     */
    save: (props) => {
        return (
            <div className={ props.className }>
                <div id="home-banner" style={{backgroundImage: `url(${ props.attributes.mediaURL })`}}>
                   {/* Render saved content here */}
                </div>
            </div>
        );
    },
});

Answer №1

After numerous hours of experimentation, I have finally cracked the code! Here is my rough draft of what I was aiming for, and it actually works! Check out this tutorial I stumbled upon: link.

// Utilizing code libraries for this block
import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
import { RichText } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';

// Block registration
registerBlockType( 'test-block/custom-repeater-block', {
    title: __('Repeater Block'),
    icon: 'layout',
    category: 'layout',
    keywords: [
        __('Custom Block'),
    ],
    attributes: {
        info: {
            type: 'array',
            selector: '.info-wrap'
        }
    },

    // Edit function
    edit: (props) => {
        const {
            attributes: { info = [] },
            setAttributes, className
        } = props;

        const infoList = (value) => {
            return(
                value.sort((a, b) => a.index - b.index).map(infoItem => {
                    return(
                        <div className="info-item">
                            <Button
                                className="remove-item"
                                onClick={ () => {
                                    const newInfo = info.filter(item => item.index != infoItem.index).map(i => {
                                        if(i.index > infoItem.index){
                                            i.index -= 1;
                                        }
                                        return i;
                                    } );
                                    setAttributes({ info: newInfo });
                                } }
                            >&times;</Button>
                            <h3>Number {infoItem.index}</h3>
                            <RichText
                                tagName="h4"
                                className="info-item-title"
                                placeholder="Enter the title here"
                                value={infoItem.title}
                                style={{ height: 58 }}
                                onChange={ title => {
                                    const newObject = Object.assign({}, infoItem, {
                                        title: title
                                    });
                                    setAttributes({
                                        info: [...info.filter(
                                            item => item.index != infoItem.index
                                        ), newObject]
                                    });
                                } }
                            />
                            <RichText
                                tagName="p"
                                className="info-item-description"
                                placeholder="Enter description"
                                value={infoItem.description}
                                style={{ height: 58 }}
                                onChange={ description => {
                                    const newObject = Object.assign({}, infoItem, {
                                        description: description
                                    });
                                    setAttributes({
                                        info: [...info.filter(
                                            item => item.index != infoItem.index
                                        ), newObject]
                                    });
                                } }
                            />
                        </div>
                    )
                })
            )
        }

        return(
            <div className={className}>
                <div className="info-wrap">{infoList(info)}</div>
                <Button onClick={title => {
                    setAttributes({
                        info: [...info, {
                            index: info.length,
                            title: "",
                            description: ""
                        }]
                    });
                }}>Add Item</Button>
            </div>
        );
    },

    // Save function
    save: (props) => {
        const info = props.attributes.info;
        const displayInfoList = (value) => {
            return(
                value.map( infoItem => {
                    return(
                        <div className="info-item">
                            <RichText.Content
                                tagName="h4"
                                className="info-item-title"
                                value={infoItem.title}
                                style={{ height: 58 }}
                            />
                        </div>
                    )
                } )
            )
        }

        return(
            <div className={props.className}>
                <div className="info-wrap">{ displayInfoList(info) }</div>
            </div>
        );
    }
} )

Answer №2

If you're looking to work on more complex blocks, I've put together an example using the query attribute which you can access here: check out the GitHub repository

I strongly believe that utilizing the query attribute is the way to go for handling intricate blocks.

Here's a breakdown of the key attributes section:

attributes: {
        services: {
            type: "array",
            source: "query",
            default: [],
            selector: "section .card-block",
            query: {
                index: {
                    type: "number",
                    source: "attribute",
                    attribute: "data-index",
                },
                headline: {
                    type: "string",
                    selector: "h3",
                    source: "text",
                },
                description: {
                    type: "string",
                    selector: ".card-content",
                    source: "text",
                },
            },
        },
    },

Next, let's dive into the edit.js portion:

import produce from "immer";
import { __ } from "@wordpress/i18n";
import "./editor.scss";

import { RichText, PlainText } from "@wordpress/block-editor";

export default function Edit({ attributes, setAttributes, className }) {
    const services = attributes.services;

    const onChangeContent = (content, index, type) => {
        const newContent = produce(services, (draftState) => {
            draftState.forEach((section) => {
                if (section.index === index) {
                    section[type] = content;
                }
            });
        });
        setAttributes({ services: newContent });
    };
    return (
        <>
            {services.map((service) => (
                <>
                    <RichText
                        tagName="h3"
                        className={className}
                        value={service.headline}
                        onChange={(content) =>
                            onChangeContent(content, service.index, "headline")
                        }
                    />
                    <PlainText
                        className={className}
                        value={service.description}
                        onChange={(content) =>
                            onChangeContent(content, service.index, "description")
                        }
                    />
                </>
            ))}
            <input
                className="button button-secondary"
                type="button"
                value={__("Add Service", "am2-gutenberg")}
                onClick={() =>
                    setAttributes({
                        services: [
                            ...attributes.services,
                            { headline: "", description: "", index: services.length },
                        ],
                    })
                }
            />
        </>
    );
}

Finally, for saving purposes, we have the following code snippet:

export default function save({ attributes, className }) {
    const { services } = attributes;

    return (
        <section className={className}>
            {services.length > 0 &&
                services.map((service) => {
                    return (
                        <div className="card-block" data-index={service.index}>
                            <h3>{service.headline}</h3>
                            <div className="card-content">{service.description}</div>
                        </div>
                    );
                })}
        </section>
    );
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Confirm that the array contains exactly 2 elements

Is there a way to confirm that an array contains exactly two specific values, for example: ['foo', 'bar'] After trying different approaches, the closest solution I found looks like this: Yup.array() .of(Yup.mixed().oneOf(['foo&a ...

Tips for displaying dynamic content based on conditions in React

I am currently working on adjusting the boilerplate repository in order to render different pages based on whether a user is logged in or not. The current setup always displays the same page but includes additional content if there is an authenticated user ...

Add the child's input query first and then concentrate on it

I have successfully appended a div with a child input, but I am facing an issue where the newly appended input is not getting focused when added. $(document).ready(function() { var max_fields = 10; //maximum input boxes allowed var wrapper ...

Are you in need of a JavaScript data validation tool?

Trying to find a library that can validate input in a specific format, such as: { points: array of { x: positive and less than 20, y: positive and less than 15 } } Ideally, it should work on both server and client sides and either return a boolean or th ...

What is the best way to run a code block every time a new reaction is added in discord.js?

I need assistance with creating a bot that can count the number of specific reactions ('⚪') to a message within a specified time frame. Additionally, I want to be able to skip the remaining time by reacting to a different emoji ('X'). ...

Using Socket.IO in Node.js to distribute information to every connected client

Currently, I am in the process of developing a WebGL multiplayer game. My approach involves using socket.io and express in node.js to enable multiplayer functionality. However, I am encountering an issue with broadcasting key events. When a user presses a ...

Solving the Issue of Assigning a Random Background Color to a Dynamically Created Button from a Selection of Colors

Trying to create my own personal website through Kirby CMS has been both challenging and rewarding. One of the features I'm working on is a navigation menu that dynamically adds buttons for new pages added to the site. What I really want is for each b ...

Enabling draggable behavior for div elements on Mozilla Firefox

I've attempted the code mentioned in this forum but unfortunately, it's not working for me. I am currently using Firefox 15 and it seems to work fine in Chrome. Below is my code : <!DOCTYPE html> <head> <title>A Simple Dragg ...

Moment JS initialization and the utc() function

I am trying to comprehend the way Moment JS initializes its moment object. For instance, let's say I want to create a moment for the date and time: April 1, 2000, 3:25:00 AM with a UTC offset of +8 hours from UTC/GMT. To represent this in JavaScript ...

Using Bootstrap multiselect, you can easily control the display of a second menu based on the selection in the first menu

On my website, I am working with two menus. When I select Abon from the first menu, it should display all li elements under the Abon optgroup (Abon-1, Abon-2). If I uncheck block in the second menu, those elements should disappear. The code consists of Sel ...

What is the best way to ensure a cron job executing a Node.js script can access variables stored in an .env file?

Currently, I have a scheduled job using cron that runs a Node.js script. This script utilizes the dotenv package to access API keys stored in a .env file. Running the Node.js script from the command line retrieves the variables from the .env file successf ...

Encountered an Error: The JSON response body is malformed in a NextJS application using the fetch

Running my NextJS app locally along with a Flask api, I confirmed that the Flask is returning proper json data through Postman. You can see the results from the get request below: { "results": [ [ { "d ...

Having trouble establishing an HTTPS connection with a Node.js server

After connecting to my server through the console without any errors, I encountered an issue in Chrome where it displayed a message stating This site can’t provide a secure connection local host uses an unsupported protocol. Here is the code snippet: im ...

Extracting an ID value from a select box in Vue.js

I'm attempting to extract the value of idTipoExame from the following JSON: { "idTipoExame": "11", "mnemonico": "AUR", "exame": "ACIDO URICO" }, { "idTipoExame": "24&qu ...

Unable to establish communication with server. Facing issues in connecting AngularJS with NodeJS

I am currently working on establishing a communication process between my server and client to receive either a "success" or "failure" response. The server is being developed using node.js with the express framework, while the client is built using angular ...

How can I create a clickable <div> element containing multiple links and trigger only one action?

Within my code, there is a <div> element that has been made clickable. Upon clicking this <div>, the height and text expand using the jQuery function $(div).animate();. While this functionality works as intended, I am encountering a slight issu ...

Creating Virtual Nodes from a string with HTML tags in Vue 2.5: A Step-by-Step Guide

Having some trouble creating a functional component that displays the Feather Icons package - I'm stuck on the final step. Here's what I have so far: This is my FeatherIcon.vue component. <script> const feather = require("feather-icons"); ...

The default zoom setting for ng-map is overly magnified when paired with the zoom-to-include-markers="true" feature

When using maps, I encountered an issue where having only one marker with the zoom-to-include-markers="true" attribute resulted in the map being overly zoomed in. Regardless of how I adjusted the zoom attribute, the result looked like this: https://i.sstat ...

What is the best way to create an Office Script autofill feature that automatically fills to the last row in Excel?

Having trouble setting up an Excel script to autofill a column only down to the final row of data, without extending further. Each table I use this script on has a different number of rows, so hardcoding the row range is not helpful. Is there a way to make ...

Tips for locating the highest number in JavaScript

I'm having trouble with my code where the first number, even if it's the largest, is not displaying as such. Instead, it shows the second largest number. Even when following suggestions, I encountered an issue where if the numbers are entered as ...