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

What is preventing me from binding ng-click to my element?

I've encountered an issue where the ng-click event is not activating on my element. Despite using the in-line controller script as shown below, I am unable to trigger my alert. Are there any integration issues that I should be mindful of? What could p ...

What is the best way to transfer data from Material UI to Formik?

I'm facing an issue when trying to integrate a Material UI 'Select' component into my Formik form. It seems like I am unable to pass the selected values from the Material UI component to Formik's initialValues. const [selectedHours, se ...

Leveraging useEffect and useContext during data retrieval

I'm currently in the process of learning how to utilize React's Context API and Hooks while working on a project that involves using fetch(). Although I am able to make the request successfully, I encounter an issue where I can't retrieve t ...

Changing URI to File Object using JavaScript

I have successfully retrieved the URI of a selected file in my Ionic app using the cordova-fileChooser plugin. Next, I used the cordova-plugin-filepath to obtain the absolute path of the file from the nativeURL on the phone. Now, the challenge is how do ...

Is there a way for me to duplicate a complex element multiple times within the same page?

As an illustration, let's say I have a social tab located in my header that I would like to duplicate in the footer. This tab is comprised of various buttons with SVG images on top, event listeners linked to button IDs, and CSS styling. One option is ...

Testing an Angular factory that relies on dependencies using JasmineJS

I have a factory setup like this: angular.module("myServices") .factory("$service1", ["$rootScope", "$service2", function($rootScope, $service2){...})]; Now I'm attempting to test it, but simply injecting $service1 is resulting in an &ap ...

Using SASS variables in JavaScript: A guide

My JavaScript file contains a list of color variables as an Object. export const colors = [ { colorVariable: "$ui-background" }, { colorVariable: "$ui-01" }, { colorVariable: "$ui-02" }, ... ] The Object above is derived from a scss file whic ...

Unable to transmit the element identifier information through ajax

Whenever I attempt to pass id data through ajax, I encounter an undefined variable error. The ajax function works smoothly with form data, but the issue arises when trying to extract the id value. Below is the PHP/HTML code snippet: <ul class="sub-men ...

Apache causes HTML download tag to malfunction

I have an HTML file that includes the following code: <a href="/Library/WebServer/Documents/file.zip" download="file.zip"> Download here </a> When I test this HTML page on Chrome, it successfully allows me to download the file. However, when ...

Accessing HP ALM with REST and JavaScript on a local server: A step-by-step guide

I am trying to access ALM using locally written JavaScript in the browser (IE11, Firefox) through the REST API, but I am unable to login. Below is the code snippet where I am attempting to request the LWSSO cookie with jQuery: var auth = btoa(USER+":"+PAS ...

How to extract keys with the highest values in a JavaScript hashmap or object

Here is an example for you to consider: inventory = {'Apple':2, 'Orange' :1 , 'Mango' : 2} In this inventory, both Apple and Mango have the maximum quantity. How can I create a function that returns both Apple and Mango as t ...

Tips for accessing private variables

After running this code in nodejs, I noticed that the 'Count' becomes negative for large values of 'count'. It made me wonder, is it the fault of 'count' or 'chain'? What would be the best approach to modify the &apo ...

Is there a way to identify and retrieve both the initial and final elements in React?

Having an issue with logging in and need to retrieve the first and last element: Below is my array of objects: 0: pointAmountMax: 99999 pointAmountMin: 1075 rateCode: ['INAINOW'] roomPoolCode: "ZZAO" [[Prototype]]: Object 1: pointAmoun ...

How can we activate navigation on a mobile browser?

I am currently working on a small HTML5/JavaScript website designed to be accessed by mobile browsers on devices such as Android and iPhone. I am utilizing the geolocation feature of HTML5 to obtain the user's current location, but my goal is to then ...

The necessary data is missing in the scope of the callback function

I'm facing an issue with a callback function's variable losing its scope. Consider the following simplified array of two objects: const search = [{socket: new WebSocket('ws://live.trade/123')}, {socket: new WebSocket( ...

When I tried to retrieve the value by using deferred.resolve(value) in my .then() method, it

I am currently utilizing Q.js to make an API call with two loops in my main function structured like this: for i..10 for i...5 var promise = getLoc(x,y); promise.then(function(value) { //value is undefined... ...

Using Node.js and Less to dynamically select a stylesheet source depending on the subdomain

Currently, my tech stack consists of nodejs, express, jade, and less. I have set up routing to different subdomains (for example: college1.domain.com, college2.domain.com). Each college has its own unique stylesheet. I am looking for a way to selectively ...

Errors in socket.on Listeners Result in Inaccurate Progress and Index Matching for Multiple Video Uploads

Is there a way to make sure that the `socket.on('upload-progress')` listener accurately updates the upload progress for each video as multiple videos are uploaded simultaneously? Currently, it appears that the listener is updating the progress fo ...

Are you ready to create a Modal Factory?

For a while now, I have been utilizing modals in various front-end frameworks to communicate with users in my applications. Typically, the process involves defining the modal's html and then rendering it through a click event. As my apps continue to ...

Ways to customize the border color on a Card component using React Material UI

Is there a way to change the border color of a Card component in React Material UI? I attempted using the style property with borderColor: "red" but it did not work. Any suggestions on how to achieve this? ...