Search code examples
javascriptwordpressjsxwordpress-gutenberggutenberg-blocks

Gutenberg repeater blocks using ESNEXT


I've been working on creating my own custom Gutenberg repeater block with a text & link input field. I've only seen ES5 samples like this and this. I've been working on creating my own version of those samples for close to 8 hours now and I'm stuck.

I'm here because I want to be pointed in the right direction (help is greatly needed).

Here is the code I currently have. I don't know where to start with the ES5 -> ESNEXT conversion.

Edit: I forgot to say that I'm trying to avoid using ACF for this

// Importing code 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', // 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 relates to 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 {Mixed} 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 });
        };

        // console.log(items);
        // var itemList = items.sort

        return (
            <div className={className}>
                <div id="#home-banner">
                    <MediaUpload
                        onSelect={onSelectImage}
                        allowedTypes="image"
                        value={mediaID}
                        render={({ open }) => (
                            <Button className={mediaID ? 'image-button' : 'button button-large'} onClick={open}>
                                {!mediaID ? __('Upload Image', 'ccm-blocks') : <img src={mediaURL} alt={__('Featured Image', 'ccm-blocks')} />}
                            </Button>
                        )}
                    />

                    <RichText
                        tagName="h1"
                        placeholder={__('Insert Title Here', 'ccm-blocks')}
                        className={className}
                        onChange={onChangeTitle}
                        value={title}
                    />

                    <RichText
                        tagName="p"
                        placeholder={__('Insert your short description here...', 'ccm-blocks')}
                        className={className}
                        onChange={onChangeContent}
                        value={content}
                    />

                    <RichText
                        tagName="ul"
                        multiline="li"
                        className="banner-buttons"
                        placeholder={ __('Add a banner button link (max of 2)', 'ccm-blocks') }
                        onChange={ onChangeBannerButtons }
                        value={ bannerButtons }
                    />
                </div>
            </div>

        );
    },

    /**
     * The save function determines how the different attributes should be combined into the final markup.
     * Which is then serialised into the post_content.
     *
     * @link https://wordpress.org/gutenberg/handbook/block-api/block-edit-save/
     *
     * @param {Object} props Props.
     * @returns {Mixed} JSX Frontend HTML.
     */
    save: (props) => {
        return (
            <div className={ props.className }>
                <div id="home-banner" style={{backgroundImage: `url(${ props.attributes.mediaURL })`}}>
                    <div class="container">
                        <div class="row">
                            <div class="col-12">
                                <div class="content-inner">
                                    <RichText.Content tagName="h1" className={ props.className } value={ props.attributes.title } />
                                    <RichText.Content tagName="p" className={ props.className } value={ props.attributes.content } />
                                    <RichText.Content tagName="ul" className="banner-buttons" value={ props.attributes.bannerButtons } />
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        );
    },
});

Edit 2: Here's my failed take on it

// Importing code 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 Block
 * 
 * @link https://wordpress.org/gutenberg/handbook/block-api/
 * @param  {string}   name     name.
 * @param  {Object}   settings settings.
 * @return {?WPBlock}          The block, otherwise `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 relates to 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 {Mixed} 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 });
        };

        // Clone an array of objects
        function _cloneArray(arr) {
            if (Array.isArray(arr)) {
              for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) {
                arr2[i] = arr[i];
              }
              return arr2;
            } else {
              return Array.from(arr);
            }
        }
        // return repeater items
        var itemList = items.sort(function(a, b){
            return a.index - b.index;
        }).map(function(item){
            console.log(item);
            return(
                <RichText
                    tagName="h1"
                    placeholder={ __('Test', 'ccm-blocks') }
                    value={ item.title }
                    onChange={ function(value){
                        var newObject = Object.assign({}, item, {
                            title: value
                        });
                        setAttributes({
                            items: [].concat(_cloneArray(items.filter(function(itemFilter){
                                return itemFilter.index != item.index;
                            })), [newObject])
                        });
                    } }
                />
            );
        });

        // 
        console.log(itemList);

        return (
            <div className={className}>
                <div id="#home-banner">
                    <RichText
                        className="item-list"
                        tagName="h1"
                        value={ itemList }
                    />

                    <Button
                        className="button add-row"
                        onClick={ function(){
                            setAttributes({
                                items: [].concat(_cloneArray(items), [{
                                    index: items.length,
                                    title: ""
                                }])
                            });
                        } }
                    >
                        Add a button
                    </Button>

                    <MediaUpload
                        onSelect={onSelectImage}
                        allowedTypes="image"
                        value={mediaID}
                        render={({ open }) => (
                            <Button className={mediaID ? 'image-button' : 'button button-large'} onClick={open}>
                                {!mediaID ? __('Upload Image', 'ccm-blocks') : <img src={mediaURL} alt={__('Featured Image', 'ccm-blocks')} />}
                            </Button>
                        )}
                    />

                    <RichText
                        tagName="h1"
                        placeholder={__('Insert Title Here', 'ccm-blocks')}
                        className={className}
                        onChange={onChangeTitle}
                        value={title}
                    />

                    <RichText
                        tagName="p"
                        placeholder={__('Insert your short description here...', 'ccm-blocks')}
                        className={className}
                        onChange={onChangeContent}
                        value={content}
                    />

                    <RichText
                        tagName="ul"
                        multiline="li"
                        className="banner-buttons"
                        placeholder={ __('Add a banner button link (max of 2)', 'ccm-blocks') }
                        onChange={ onChangeBannerButtons }
                        value={ bannerButtons }
                    />
                </div>
            </div>

        );
    },

    /**
     * The save function determines how the different attributes should be combined into the final markup.
     * Which is then serialised into the post_content.
     *
     * @link https://wordpress.org/gutenberg/handbook/block-api/block-edit-save/
     *
     * @param {Object} props Props.
     * @returns {Mixed} JSX Frontend HTML.
     */
    save: (props) => {
        return (
            <div className={ props.className }>
                <div id="home-banner" style={{backgroundImage: `url(${ props.attributes.mediaURL })`}}>
                    <div class="container">
                        <div class="row">
                            <div class="col-12">
                                <div class="content-inner">
                                    <RichText.Content tagName="h1" className={ props.className } value={ props.attributes.title } />
                                    <RichText.Content tagName="p" className={ props.className } value={ props.attributes.content } />
                                    <RichText.Content tagName="ul" className="banner-buttons" value={ props.attributes.bannerButtons } />
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        );
    },
});

Solution

  • I've figured it out!! After countless hours of tinkering with it here's what I came up with. It's a rough version of what I want but it definitely works! Here's a link for one of the tutorials I found link.

    // Importing 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';
    
    // Register the block
    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>
            );
        }
    } )