3

I feel like I'm so close to just wrapping up this custom Gutenberg block.

What works:

  • Adding new block
  • Editing block / adding custom inner blocks
  • Saving block
  • Viewing block on frontend

What's broken:

  • After saving block, if I refresh the editor page, the InnerBlock components are missing

code example

Essentially, the InnerBlock data is being saved but is not being rendered in the backend after page refresh. I've been staring at this documentation for so long, there's not many resources for coding in just plain JavaScript for InnerBlocks.

Edited code snippets on 26 August to include attributes and my latest attempts at resolving the problem.

I am using plain JavaScript, not JSX!

Parent Script

(function(blocks, element, blockEditor) {

    const { registerBlockType } = blocks;
    const el = element.createElement;
    const useBlockProps = blockEditor.useBlockProps;
    const InnerBlocks = blockEditor.InnerBlocks;
    const useInnerBlocksProps = blockEditor.useInnerBlocksProps;

    registerBlockType('aurise/video', {
        title: 'Video',
        description: 'Lorem ipsum',
        category: 'aurise', //[ text | media | design | widgets | theme | embed ]

        /* Block Settings */
        attributes: {
            
            //About the Video
            id: {
                type: 'string',
                default: ''
            },
            title: {
                type: 'string',
                default: ''
            },
            width: {
                type: 'integer',
                default: 1280
            },
            height: {
                type: 'integer'
            },
            poster_url: {
                type: 'string',
                default: ''
            },
            upload_date: {
                type: 'string',
                default: ''
            },
            duration: {
                type: 'string',
                default: '',
            },
            autoplay: {
                type: 'string',
                default: ''
            },
            captions: {
                type: 'string',
                default: 'on'
            },

            //About the Author
            author_name: {
                type: 'string',
                default: ''
            }
        },

        edit: function(props, setAttributes, className) {

            function update_video_id(event) {
                props.setAttributes({ id: event.target.value });
            };

            function update_title(event) {
                props.setAttributes({ title: event.target.value });
            };

            function update_width(event) {
                props.setAttributes({ width: event.target.value });
            };

            function update_height(event) {
                props.setAttributes({ height: event.target.value });
            };

            function update_poster_url(event) {
                props.setAttributes({ poster_url: event.target.value });
            };

            function update_upload_date(event) {
                props.setAttributes({ upload_date: event.target.value });
            };

            function update_duration(event) {
                props.setAttributes({ duration: event.target.value });
            };

            function update_autoplay(event) {
                if (event.target.checked) {
                    props.setAttributes({ autoplay: 'on' });
                } else {
                    props.setAttributes({ autoplay: '' });
                }
            };

            function update_captions(event) {
                if (event.target.checked) {
                    props.setAttributes({ captions: 'on' });
                } else {
                    props.setAttributes({ captions: '' });
                }
            };

            function update_author_name(event) {
                props.setAttributes({ author_name: event.target.value });
            };

            let blockProps = useBlockProps(),
                innerBlockProps = useInnerBlocksProps(blockProps),
                output = el(
                    //Block Header
                    'div', { className: 'au-video au-video-youtube' },
                    el(
                        'h3',
                        null,
                        'Embedded YouTube Video'
                    ),
                    //Row with Video ID and Title
                    el(
                        'div', { className: 'au-row' },
                        //Video ID
                        el(
                            'label', { className: 'col-xs-12 col-md-6' },
                            el(
                                'span', { className: 'au-input-label' },
                                'Video ID'
                            ),
                            el('input', {
                                type: 'text',
                                required: 'required',
                                value: props.attributes.id,
                                onChange: update_video_id
                            })
                        ),
                        //Video Title
                        el(
                            'label', { className: 'col-xs-12 col-md-6' },
                            el(
                                'span', { className: 'au-input-label' },
                                'Title'
                            ),
                            el('input', {
                                type: 'text',
                                value: props.attributes.title,
                                onChange: update_title
                            })
                        )
                    ),
                    //Row with video's height and width
                    el(
                        'div', { className: 'au-row' },
                        //Video Width
                        el(
                            'label', { className: 'col-xs-12 col-md-6' },
                            el(
                                'span', { className: 'au-input-label' },
                                'Width (in pixels)'
                            ),
                            el('input', {
                                type: 'number',
                                required: 'required',
                                step: 1,
                                min: 0,
                                placeholder: 1280,
                                value: props.attributes.width,
                                onChange: update_width
                            })
                        ),
                        //Video Height
                        el(
                            'label', { className: 'col-xs-12 col-md-6' },
                            el(
                                'span', { className: 'au-input-label' },
                                'Height (in pixels)'
                            ),
                            el('input', {
                                type: 'number',
                                step: 1,
                                min: 0,
                                placeholder: 720,
                                value: props.attributes.height,
                                onChange: update_height
                            })
                        )
                    ),
                    //Row with video's upload date and duration
                    el(
                        'div', { className: 'au-row' },
                        //Duration
                        el(
                            'label', { className: 'col-xs-12 col-md-6' },
                            el(
                                'span', { className: 'au-input-label' },
                                'Total Video Duration (in seconds)'
                            ),
                            el('input', {
                                type: 'text',
                                placeholder: '2640',
                                value: props.attributes.duration,
                                onChange: update_duration
                            })
                        ),
                        //Upload Date
                        el(
                            'label', { className: 'col-xs-12 col-md-6' },
                            el(
                                'span', { className: 'au-input-label' },
                                'Date video was Uploaded'
                            ),
                            el('input', {
                                type: 'date',
                                value: props.attributes.upload_date,
                                onChange: update_upload_date
                            })
                        )
                    ),
                    //Row with poster image
                    el(
                        'div', { className: 'au-row' },
                        //Poster Image
                        el(
                            'label', { className: 'col-xs-12' },
                            el(
                                'span', { className: 'au-input-label' },
                                'Poster Image (insert from URL)'
                            ),
                            el('input', {
                                type: 'text',
                                value: props.attributes.poster_url,
                                placeholder: 'https://via.placeholder.com/1280x720',
                                onChange: update_poster_url
                            })
                        )
                    ),
                    //Row with autoplay & caption checkboxes
                    el(
                        'div', { className: 'au-row' },
                        //Autoplay
                        el(
                            'label', { className: 'col-xs-12 col-md-6' },
                            el('input', {
                                type: 'checkbox',
                                value: 'autoplay',
                                checked: props.attributes.autoplay ? 'checked' : '',
                                onChange: update_autoplay
                            }),
                            el(
                                'span', { className: 'au-input-label' },
                                'autoplay video'
                            )
                        ),
                        //Captions
                        el(
                            'label', { className: 'col-xs-12 col-md-6' },
                            el('input', {
                                type: 'checkbox',
                                value: 'closed-captions',
                                checked: props.attributes.captions ? 'checked' : '',
                                onChange: update_captions
                            }),
                            el(
                                'span', { className: 'au-input-label' },
                                'display closed captions by default if available'
                            )
                        )
                    ),
                    //Row with author
                    el(
                        'div', { className: 'au-row' },
                        //Author
                        el(
                            'label', { className: 'col-xs-12 col-md-6' },
                            el(
                                'span', { className: 'au-input-label' },
                                'Name of Video\'s Author'
                            ),
                            el('input', {
                                type: 'text',
                                value: props.attributes.author_name,
                                onChange: update_author_name
                            })
                        )
                    ),
                    //Row with video clips
                    el('div', { className: 'au-video-clips' },
                        el(
                            'h4',
                            null,
                            'Video Clips'
                        ),
                        el('div', innerBlockProps, el(InnerBlocks, { allowedBlocks: ['aurise/videoclip'] }))
                    )
                );

            return output;
        },

        save: function(props) {
            let blockProps = useBlockProps.save(),
                innerBlockProps = useInnerBlocksProps.save(blockProps),
                output = '[au_youtube'; //Output the shortcode using the property attributes
            for (property in props.attributes) {
                output += ' ' + property + '="' + props.attributes[property] + '"';
            }
            output += ']';
output += el('div', blockProps, el('div', innerBlockProps, el(InnerBlocks.Content))); //Outputs: [object Object] in code editor
            output += '[/au_youtube]'; //Close parent shortcode
            console.info('[YOUTUBE VIDEO] Properties', props);
            console.info('[YOUTUBE VIDEO] Block Props', blockProps);
            console.info('[YOUTUBE VIDEO] Inner Block Props', innerBlockProps);
            console.info('[YOUTUBE VIDEO] Inner Content', InnerBlocks.Content);
            console.info('[YOUTUBE VIDEO] Output', output);
            return output;
        }

    });

})(
    window.wp.blocks,
    window.wp.element,
    window.wp.blockEditor
);

And then here's the custom child block code

(function(blocks, element, blockEditor) {

    const { registerBlockType } = blocks;
    const el = element.createElement;
    const useBlockProps = blockEditor.useBlockProps;
    const useInnerBlocksProps = blockEditor.useInnerBlocksProps;

    registerBlockType('aurise/videoclip', {
        parent: ['aurise/video'],
        title: 'Video Clip',
        description: 'Lorem ipsum',
        category: 'aurise', //[ text | media | design | widgets | theme | embed ]

        /* Block Settings */
        attributes: {
            name: {
                type: 'string',
                default: ''
            },
            description: {
                type: 'string',
                default: ''
            },
            startOffset: {
                type: 'integer',
                default: 0
            },
            endOffset: {
                type: 'integer',
                default: 0
            }
        },

        edit: function(props) {

            function update_name(event) {
                props.setAttributes({ name: event.target.value });
            };

            function update_description(event) {
                props.setAttributes({ description: event.target.value });
            };

            function update_start(event) {
                props.setAttributes({ startOffset: event.target.value });
            };

            function update_end(event) {
                props.setAttributes({ endOffset: event.target.value });
            };

            let blockProps = useBlockProps(),
                innerBlockProps = useInnerBlocksProps(blockProps),
                output = el(
                    'div', { className: 'au-video au-video-clip' },
                    el(
                        'h3',
                        null,
                        'Video Clip'
                    ),
                    //Row with Name and Description
                    el(
                        'div', { className: 'au-row' },
                        //Clip Name
                        el(
                            'label', { className: 'col-xs-12 col-md-6' },
                            el(
                                'span', { className: 'au-input-label' },
                                'Name'
                            ),
                            el('input', {
                                type: 'text',
                                required: 'required',
                                value: props.attributes.name,
                                onChange: update_name
                            })
                        ),
                        //Clip Description
                        el(
                            'label', { className: 'col-xs-12 col-md-6' },
                            el(
                                'span', { className: 'au-input-label' },
                                'Description'
                            ),
                            el('input', {
                                type: 'text',
                                value: props.attributes.description,
                                onChange: update_description
                            })
                        )
                    ),
                    //Row with Start and End offsets
                    el(
                        'div', { className: 'au-row' },
                        //Clip Start Offset
                        el(
                            'label', { className: 'col-xs-12 col-md-6' },
                            el(
                                'span', { className: 'au-input-label' },
                                'Start Time (in seconds)'
                            ),
                            el('input', {
                                type: 'number',
                                required: 'required',
                                step: 1,
                                min: 0,
                                placeholder: 20,
                                value: props.attributes.startOffset,
                                onChange: update_start
                            })
                        ),
                        //Clip End Offset
                        el(
                            'label', { className: 'col-xs-12 col-md-6' },
                            el(
                                'span', { className: 'au-input-label' },
                                'End Time (in seconds)'
                            ),
                            el('input', {
                                type: 'number',
                                required: 'required',
                                step: 1,
                                min: 0,
                                placeholder: 25,
                                value: props.attributes.endOffset,
                                onChange: update_end
                            })
                        )
                    )
                );

            return output;
        },

        save: function(props) {
            let blockProps = useBlockProps.save(),
                innerBlockProps = useInnerBlocksProps.save(blockProps),
                output = '[au_video_clip'; //Open shortcode
            //Output the shortcode using the property attributes
            for (property in props.attributes) {
                output += ' ' + property + '="' + props.attributes[property] + '"';
            }
            output += ' from-clip="true"]'; //Close shortcode
            console.info('[VIDEO CLIP] Properties', props);
            console.info('[VIDEO CLIP] Block Props', blockProps);
            console.info('[VIDEO CLIP] Inner Block Props', innerBlockProps);
            console.info('[VIDEO CLIP] Output', output);
            return output;
        }

    });

})(
    window.wp.blocks,
    window.wp.element,
    window.wp.blockEditor
);

As far as I understand, the save function in the child block isn't actually being hit in the backend, which is why I rendered the shortcode using the props in the parent. But I think that's hacky and unsure what to do? Like... it seems the data is being saved and shows up on the frontend just fine, but I can't edit it since they don't load up on the backend.

By adding innerBlockProps = useInnerBlocksProps.save(blockProps) to the parent block's save function, I can finally see the console info lines from the child showing up in the log so I can see it's actually triggering that now. I've removed my hacky bit to use InnerBlocks.Content as documentation says, but it still outputs [object Object] in the editor instead of what the child block's output which is a string, wouldn't matter if it was a shortcode or not. I also added it to the child block's save function thinking maybe that'll do it?? At any rate, I think the solution is somewhere between the child's save function and the parent's edit and/or save function.

Any help is appreciated!

AuRise
  • 2,253
  • 19
  • 33

1 Answers1

0

Looking at your code, without all the attributes and functions, unfortunately it cannot be tested as is.. but I'd like to try help you anyhow.

In Parent Script, there is no opening { for edit: function(props) but there is a closing }, before save(). This may by why you are not seeing anything being saved as the registerBlockType(...) function terminates early.

If the missing brace is a typo in the example code, the next steps I'd try to resolve it would be:

  1. Test the child block aurise/videoclip by itself, remove its parent property and check that it functions and saves as expected independently.
  2. Check the browser console for any errors when you are updating properties in the block and saving it.
  3. Always force refresh the browser/clear the cache when testing/looking for issues.

A different approach to your block could be to extend the existing YouTube varation of the core/embed block to add your required attributes and classnames.

At present, both of your blocks are manually converting all their block properties into the shortcode format to be saved. As all the blocks attributes are not sourced from the saved markup, I would avoid formatting the attributes into a shortcode, instead letting Gutenberg serialize the block as a block (much safer/less error prone). I would suggest what your block is trying to achieve is similiar to Creating Dynamic Blocks, possibly removing the need for using InnerBlocks. Instead of saving a [shortcode] via a Gutenberg block, the ServerSideRender component can call your existing PHP function to render the content in the Block Editor and front end using the blocks attributes.

S.Walsh
  • 3,105
  • 1
  • 12
  • 23
  • Yeah, the lack of opening curly brace for parent's edit function is a typo, it exists in my code. I was poking around the source code for the blockEditor and discovered that adding `const useInnerBlocksProps = blockEditor.useInnerBlocksProps;` to the top with the other constants and then adding `let blockProps = useBlockProps.save(), innerBlockProps = useInnerBlocksProps.save(blockProps);` in the save function of the parent did trigger the save function for the child, so I was able to remove the for-loop from the parent and output `InnerBlocks.Content` as best practice dictates – AuRise Aug 26 '22 at 15:13
  • But the issue that remains is appropriately loading the saved child content in the backend after page refresh. It still just shows up blank. I'll update the code in the question with the attributes to see if that can further help you help me! – AuRise Aug 26 '22 at 15:16
  • 1
    Ah, I figured it out! Thanks to the resources you linked in there and my own stupid comment saying I was returning a string, I did realize that the save function is _SUPPOSED_ to return an object, specifically an object resulting from `el()` – AuRise Aug 26 '22 at 17:46
  • 1
    @AuRise No worries! Glad to hear you were able to figure it out. Thanks for sharing the process you went through to find the resolution, its really helpful to know what you tried incase others come across the same issue. – S.Walsh Aug 30 '22 at 11:54