1

EDIT: Simply put, the issue I'm having is that the image data object (imgDataObj in the js), which is saved as an attribute in the JS side, is getting passed to the PHP side even though I'm not passing it in the serversiderender component and I'm not listening for it in the PHP render callback.

I have a custom script that creates image srcset with custom sizes. I'm trying to put this into a block. ServerSideRender seems to be the way to go. I've made a handful of blocks using it but never one with an image. So the idea is, the user uploads/picks an image from the media library and sets their custom sizes for each breakpoint. The block sends the sizes and just the ID number of the image to the PHP side of things. We don't need the entire image object. For one it creates an error say the URL is too long. All my script needs is the ID. However, even though I'm not passing the image object in the attributes, it shows up in the PHP side.

I got this working in a non-serversiderender block but it was kinda clunky. Serversiderender seems more elegant but I'm just missing something.

Here is my js:

import React from 'react';

if (wp.element && wp.editor) {
    const { __ } = wp.i18n; // Import __() from wp.i18n
    const {
        registerBlockType,
        getBlockDefaultClassName
    } = wp.blocks;

    const {
        useBlockProps,
        MediaUploadCheck,
    } = wp.blockEditor;

    const {
        Button,
        PanelBody,
        PanelRow,
        SelectControl,
        TextControl,
        ToggleControl,
    } = wp.components;
    
    const {
        InspectorControls,
        MediaUpload
    } = wp.editor;

    const { serverSideRender: ServerSideRender } = wp;

    const el = wp.element.createElement;

    const icon = el('svg',{
            xmlns: 'http://www.w3.org/2000/svg',
            viewBox: '0 0 93 93',
        },
        el('title', 'icon' ),
        el('g',{
            id: 'Layer_2',
            'data-name': 'Layer 2'
        },
            el('g',{
                id: 'Layer_1-2',
                'data-name': 'Layer 1'
            },
                el('path',{
                    d: 'M30,30V93H93V30ZM80.47,61l-19,19L50.66,69.11,36,83.77V36H87V67.52Z'
                }),
                el('circle',{
                    cx: '46.21',
                    cy: '46.53',
                    r: '5.76',
                }),
                el('polygon', {
                    points: '6 87 6 6 87 6 87 13 93 13 93 0 0 0 0 93 13 93 13 87 6 87'
                }),
                el('polygon', {
                    points: '21 87 21 21 87 21 87 28 93 28 93 15 15 15 15 93 28 93 28 87 21 87'
                }),
            )
        )
    );

    registerBlockType('cw-blocks/responsive-image', {
        title: 'Responsive Image',
        icon: icon,
        category: 'embed',
        attributes: {
            imgW: {
                type: 'number',
                default: 1600,
            },
            imgH: {
                type: 'number',
                default: 1200,
            },
            imgWMed: {
                type: 'number',
                default: 800,
            },
            imgHMed: {
                type: 'number',
                default: 600,
            },
            imgWSmall: {
                type: 'number',
                default: 400,
            },
            imgHSmall: {
                type: 'number',
                default: 300,
            },
            crop: {
                type: 'boolean',
                default: false,
            },
            linkURL: {
                type: 'string',
                default: ''
            },
            targetBlank: {
                type: 'boolean',
                default: false
            },
            pos: {
                type: 'text',
                default: ''
            },
            imgDataObj: {
                type: 'array',
                default: {}
            },
            imgID: {
                type: 'string',
                default: ''
            }
        },

        edit: (props) => {
            const blockProps = useBlockProps();
            const { className, setAttributes } = props;
            const { attributes } = props;

            const setImgData = function (imgDataObj){
                setAttributes({ imgDataObj: imgDataObj });
                setAttributes({ imgID: imgDataObj['id'] });
            }

            const resetImgData = function (){
                setAttributes({ imgDataObj: {} });
                setAttributes({ imgID: '' });
            }

            return [
                <InspectorControls>
                    <PanelBody title="Image" initialOpen={true}>
                        <MediaUploadCheck>
                            <MediaUpload
                                className="cw-resp-image wp-admin-cw-resp-image"
                                allowedTypes={['image']}
                                multiple={false}
                                value={props.attributes.imgDataObj ? props.attributes.imgDataObj.id : ''}
                                onSelect={setImgData}
                                render={({ open }) => (
                                    attributes.imgDataObj.id ?
                                        <div>
                                            <p>
                                                <img src={attributes.imgDataObj.url} width={attributes.imgDataObj.width / 2} />
                                            </p>

                                            <p>
                                                <Button onClick={resetImgData} className="button is-small">Remove</Button>
                                            </p>
                                        </div> :
                                        <Button onClick={open} className="button">Select/Upload Image</Button>
                                )}
                            />
                        </MediaUploadCheck>
                        <SelectControl
                            label="Position"
                            value={props.attributes.pos}
                            onChange={(pos) => setAttributes({ pos: pos })}
                            options={[
                                { label: 'Left', value: 'left' },
                                { label: 'Center', value: 'center' },
                                { label: 'Right', value: 'right' },
                            ]}
                        />
                    </PanelBody>

                    <PanelBody title="URL" initialOpen={false}>
                        <TextControl
                            label="URL"
                            value={props.attributes.linkURL}
                            onChange={(linkURL) => setAttributes({ linkURL: linkURL })}
                            type="text"
                        />
                        <ToggleControl
                            label="Open link in new tab"
                            checked={props.attributes.targetBlank}
                            onChange={(targetBlank) => setAttributes({ targetBlank: targetBlank })}
                        />
                    </PanelBody>

                    <PanelBody title="Sizes" initialOpen={true}>
                        <PanelRow>
                            <p>Both width and height are required to create a cropped image.</p>
                        </PanelRow>

                        <TextControl
                            label="Small Width (phones)"
                            value={props.attributes.imgWSmall}
                            onChange={(imgWSmall) => setAttributes({ imgWSmall: parseInt(imgWSmall) })}
                            type="number"
                        />
                        <TextControl
                            label="Small Height (phones)"
                            value={props.attributes.imgHSmall}
                            onChange={(imgHSmall) => setAttributes({ imgHSmall: parseInt(imgHSmall) })}
                            type="number"
                        />

                        <TextControl
                            label="Medium Width (tablets)"
                            value={props.attributes.imgWMed}
                            onChange={(imgWMed) => setAttributes({ imgWMed: parseInt(imgWMed) })}
                            type="number"
                        />
                        <TextControl
                            label="Medium Height (tablets)"
                            value={props.attributes.imgHMed}
                            onChange={(imgHMed) => setAttributes({ imgHMed: parseInt(imgHMed) })}
                            type="number"
                        />

                        <TextControl
                            label="Large Width (desktop)"
                            value={props.attributes.imgW}
                            onChange={(imgW) => setAttributes({ imgW: parseInt(imgW) })}
                            type="number"
                        />
                        <TextControl
                            label="Large Height (desktop)"
                            value={props.attributes.imgH}
                            onChange={(imgH) => setAttributes({ imgH: parseInt(imgH) })}
                            type="number"
                        />
                    </PanelBody>

                    <PanelBody title="Options" initialOpen={false}>
                        <ToggleControl
                            label="Crop"
                            checked={props.attributes.crop}
                            onChange={(crop) => setAttributes({ crop: crop })}
                        />
                    </PanelBody>
                </InspectorControls>,
                <div {...blockProps}>
                    <ServerSideRender
                        block="cw-blocks/responsive-image"
                        attributes={{
                            // no image object sent here
                            imgW: attributes.imgW,
                            imgH: attributes.imgH,
                            imgWMed: attributes.imgWMed,
                            imgHMed: attributes.imgHMed,
                            imgWSmall: attributes.imgWSmall,
                            imgHSmall: attributes.imgHSmall,
                            crop: attributes.crop,
                            loading: attributes.loading,
                            linkURL: attributes.linkURL,
                            targetBlank: attributes.targetBlank,
                            pos: attributes.pos,
                            imgID: attributes.imgID,
                        }}
                    />
                </div>
            ];
        },
        save: () => {
            return null;
        },
    });
}

and here is the php

<?php

function cw_register_respimg_block(){
    wp_register_script('responsive-image', get_template_directory_uri() . '/src/js/admin/image-block.js', array(
        'wp-blocks',
        'wp'
    ));

    register_block_type(
        'cw-blocks/responsive-image',
        array(
            'attributes' => array(
                // no image object received here
                'imgW' => array(
                    'type' => 'number',
                    'default' => 1600,
                ),
                'imgH' => array(
                    'type' => 'number',
                    'default' => 1200,
                ),
                'imgWMed' => array(
                    'type' => 'number',
                    'default' => 800,
                ),
                'imgHMed' => array(
                    'type' => 'number',
                    'default' => 600,
                ),
                'imgWSmall' => array(
                    'type' => 'number',
                    'default' => 400,
                ),
                'imgHSmall' => array(
                    'type' => 'number',
                    'default' => 300,
                ),
                'crop' => array(
                    'type' => 'boolean',
                    'default' => false,
                ),
                'linkURL' => array(
                    'type' => 'string',
                    'default' => ''
                ),
                'targetBlank' => array(
                    'type' => 'boolean',
                    'default' => false
                ),
                'pos' => array(
                    'type' => 'text',
                    'default' => ''
                ),
                'imgID' => array(
                    'type' => 'string',
                    'default' => ''
                ),
            ),
            'render_callback' => 'cw_respimg_renderer',
            'editor_script' => 'responsive-image'
        )
    );
}

function cw_respimg_renderer($block_attributes, $content){
    $html = '';

    // need the image id to build custom srcset
    if($block_attributes['imgID']) {

        // build size option data for image script
        $respimg_sizes = array();
    
        if($block_attributes['imgWSmall'] || $block_attributes['imgHSmall']) {
            $respimg_sizes['respimg_size_small'] = array(
                'w' => $block_attributes['imgWSmall'] ? $block_attributes['imgWSmall'] : NULL,
                'w' => $block_attributes['imgHSmall'] ? $block_attributes['imgHSmall'] : NULL,
                'crop' => $block_attributes['crop'],
            );
        }
    
        if($block_attributes['imgWMed'] || $block_attributes['imgHMed']) {
            $respimg_sizes['respimg_size_med'] = array(
                'w' => $block_attributes['imgWMed'] ? $block_attributes['imgWMed'] : NULL,
                'w' => $block_attributes['imgHMed'] ? $block_attributes['imgHMed'] : NULL,
                'crop' => $block_attributes['crop'],
            );
        }
    
        if($block_attributes['imgW'] || $block_attributes['imgH']) {
            $respimg_sizes['respimg_size_large'] = array(
                'w' => $block_attributes['imgW'] ? $block_attributes['imgW'] : NULL,
                'w' => $block_attributes['imgH'] ? $block_attributes['imgH'] : NULL,
                'crop' => $block_attributes['crop'],
            );
        }
    
        $resp_image = get_cw_img($block_attributes['imgID'], 'respimg_size_large', $respimg_sizes);
    
        // build image link
        $link_start = '';
        $link_end = '';
    
        if($block_attributes['linkURL']) {
            $link_start = $block_attributes['targetBlank'] ? '<a href="'.$block_attributes['linkURL'].'" target="_blank">' : '<a href="'.$block_attributes['linkURL'].'">';
            $link_end = '</a>';
        }
    
        $html .= '<div class="cw-resp-img-mother '.$block_attributes['pos'].'">'.$link_start.$resp_image.$link_end.'</div>';
    }

    return $html;
}

add_action('init', 'cw_register_respimg_block');

Any help is greatly appreciated.

4
  • if($block_attributes['imgID']) { is a very unusual check to do in PHP, have you passed this through boolval somewhere that hasn't been included? I notice there is no else case providing debug information, so we can only speculate that the attribute value is missing, or maybe it's empty? or false? We have no way of knowing with the code you've provided, the PHP may be perfectly fine ( e.g. if the block itself isn't saving the imgID correctly ). How do you know that setImgData works correctly?
    – Tom J Nowell
    Aug 25 at 16:57
  • So you kinda missed my question. The issue is that the image object data, which is saved as an attribute in the JS side, is getting passed to the PHP side even though I'm not passing it serversiderender component and I'm not listening for it in the PHP render callback. I know the code needs a little finessing but I need to get over this hurdle. Aug 25 at 18:58
  • @JoelAbeyta Are you using WordPress 5.8?
    – Sally CJ
    Aug 26 at 8:36
  • 1
    @sally-cj Yes, everything is up to date. Aug 26 at 13:25
1

the issue I'm having is that the image data object (imgDataObj in the js), which is saved as an attribute in the JS side, is getting passed to the PHP side even though I'm not passing it in the serversiderender component and I'm not listening for it in the PHP render callback

That issue could happen if you're using WordPress 5.8 which uses Gutenberg 10.7 which has a function named __experimentalSanitizeBlockAttributes that's defined in the @wordpress/blocks or wp.blocks package. See lines 1255-1293 in wp-includes/js/dist/blocks.js or the source at https://github.com/WordPress/gutenberg/blob/v10.7.0/packages/blocks/src/api/utils.js#L236-L277 for Gutenberg 10.7.0 release.

And what the function does is:

/**
 * Ensure attributes contains only values defined by block type, and merge
 * default values for missing attributes.
 *
 * @param {string} name       The block's name.
 * @param {Object} attributes The block's attributes.
 * @return {Object} The sanitized attributes.
 */
export function __experimentalSanitizeBlockAttributes( name, attributes ) {

And in wp-includes/js/dist/server-side-render.js (on line 220) or the source at https://github.com/WordPress/gutenberg/blob/v10.7.0/packages/server-side-render/src/server-side-render.js#L75-L77 (for Gutenberg 10.7.0), you can see that ServerSideRender applies __experimentalSanitizeBlockAttributes() on the attributes:

const sanitizedAttributes =
    attributes &&
    __experimentalSanitizeBlockAttributes( block, attributes );

Which means that the attributes will be merged with the ones set when registering the block type, so that's exactly why in your case, the imgDataObj was passed to your render callback (in PHP) even if you didn't set the attribute in the attributes property passed to ServerSideRender.

How to fix the issue

Because you said, "We don't need the entire image object", then just remove imgDataObj from the attributes when you register the block type.

And then, add a local variable/constant in your edit function like so:

// At the top in the file, add:
const { useSelect } = wp.data;

// Then in the edit function, add this:
const imgDataObj = useSelect( select => {
    const { getEntityRecord } = select( 'core' );
    return attributes.imgID && getEntityRecord( 'root', 'media', attributes.imgID );
}, [ attributes.imgID ] );

And note that there are other changes you need to make, but see my code on GitHub for more details. (see link at the bottom)

But before that, please read the following notes..

Additional Issues/Notes

  1. InspectorControls and MediaUpload should be imported from the @wordpress/block-editor or wp.blockEditor package.

  2. text is not a valid attribute type and yet in your JS and PHP, the pos attribute used text as the type. So ensure your attributes are using the correct type.

  3. Media/post IDs are numbers, so I'd change the type of the imgID attribute to number.

  4. You should use the dot notation when accessing properties in objects, unless if the property name contains characters like spaces or hypens (-). So instead of imgDataObj['id'], I'd use imgDataObj.id.

  5. Your edit function is returning an array of elements, so each top-level element should have a unique key property, e.g. <InspectorControls key="my-key">.

  6. When you register your script in PHP (using wp_register_script()), you should specify all the WordPress/Gutenberg packages that you use in your script. So instead of array( 'wp-blocks', 'wp' ), I'd use array( 'wp-element', 'wp-editor', 'wp-blocks', 'wp-block-editor', 'wp-components', 'wp-server-side-render' ).

  7. And actually, to allow bigger attributes object, you can set httpMethod to POST like so: <ServerSideRender httpMethod="POST" .../>.

Try/Check My Code

You can find the source (ESNext + JSX) on GitHub, and check this diff to see what I changed.

5
  • Awesome! Thanks for all the info! Aug 26 at 18:49
  • So would you please accept or upvote my answer? :)
    – Sally CJ
    Aug 26 at 23:15
  • I haven't had time to look through it and test it out yet, I'll gladly do both as soon as I get time to check it out. Aug 27 at 13:56
  • Ah sorry, my bad, I should've asked whether you already tried and tested it out.. 🙈😀 Anyway, take your time. :)
    – Sally CJ
    Aug 27 at 14:12
  • 1
    Thank you Sally CJ! That got me pointed in the right direction. There is still a little way to go to to make this complete, but you got me squared away. And, actually, helped with more stuff than I asked for so that you for that! Sep 13 at 19:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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