0

I've got a WP Block with a <ServerSideRender /> component in the backend. It works fine with the attributes. But it does not return the $content used in the php callback for the frontend.

<ServerSideRender
    block="mydomain/my-block"
    attributes={ attributes }
/>

And I can't find in the documentation if there is a way to dynamicly render the block on the backend while using the $content.

The $content is a nested block, so the save() is used like:

    save() {
        return <InnerBlocks.Content />;
    },

Does anyone know how to accomplice this?

--EDIT-- I've added my current edit() code. Please note that not all is relevant. The focus lays on:

  • the use of InnerBlocks
  • the use of a preview button. one state is to edit the content and one state it to render a preview with the php callback.
  • the use of ServerSideRender
/**
 * WordPress dependencies
 */

const { __ } = wp.i18n;
const {
    InnerBlocks,
    InspectorControls,
    MediaUpload,
    MediaUploadCheck,
    BlockControls,
} = wp.blockEditor || wp.editor; // Fallback to 'wp.editor' for backwards compatibility
const {
    PanelBody,
    RangeControl,
    Button,
    SelectControl,
    ToolbarGroup,
    ToolbarButton,
    ServerSideRender,
} = wp.components;

const { Component, Fragment } = wp.element;
const { withSelect, select } = wp.data;
const { compose } = wp.compose;

class BlockImageOverlayEdit extends Component {
    render() {

        const {
            clientId,
            attributes,
            className,
            setAttributes,
            hasChildBlocks,
        } = this.props;
        const {
            id,
            preview,
            editorSize,
            bgColor,
            mediaId,
            mediaUrl,
            centerContent,
            mediaSpanCols,
            mediaSpanOffset,
            overlaySpanCols,
            overlaySpanOffset,
            alignX,
            alignY,
        } = attributes;

        if ( id !== clientId ) {
            setAttributes( { id: clientId } );
        }
        const colMaxAmount = 12;
        const sizes = [ 'xs', 'sm', 'md', 'lg', 'xl', 'xxl' ];

        const removeMedia = () => {
            this.props.setAttributes( {
                mediaId: 0,
                mediaUrl: '',
            } );
        };

        const onSelectMedia = ( media ) => {
            setAttributes( {
                mediaId: media.id,
                mediaUrl: media.url,
            } );
        };

        const onChangeColumns = ( value ) => {
            const newMediaSpanOffset = (value + mediaSpanOffset) > colMaxAmount ? colMaxAmount - value : mediaSpanOffset;
            setAttributes(
                {
                    mediaSpanCols: value,
                    mediaSpanOffset: newMediaSpanOffset,
                } );
        };
        
        const onChangeColumnsOverlay = ( value ) => {
            const newOverlaySpanOffset = (value + overlaySpanOffset) > colMaxAmount ? colMaxAmount - value : overlaySpanOffset;
            setAttributes(
                {
                    overlaySpanCols: value,
                    overlaySpanOffset: newOverlaySpanOffset,
                } );
        };


        const MyServerSideRender = () => {
// thought. maybe I can fetch, and prerender the inner blocks here, serialize them and set them as an attribute. Right before this return. (and at save() set it back to null)
            return (
                <ServerSideRender
                    block="mydomain/block-wp-block-image-overlay-01"
                    attributes={ attributes }
                />
            );
        };

        const PreviewButton = () => (
            <BlockControls>
                <ToolbarGroup>
                    <ToolbarButton
                        icon={ preview ? 'hidden' : 'visibility' }
                        label="Preview"
                        onClick={ () => setAttributes( { preview: ! preview } ) }
                    />
                </ToolbarGroup>
            </BlockControls>
        );

        const BreakpointButtons = () => (
            <ToolbarGroup>
                { sizes.map( ( size, index ) => {
                    return (
                        <ToolbarButton
                            key={ index }
                            style={ { padding: 0.5 + 'em' } } //TODO: fix spacing
                            isPrimary={ editorSize === size }
                            isSecondary={ editorSize !== size }

                            // icon={ edit }
                            label="Edit"
                            onClick={ () => setAttributes( { editorSize: size } ) }
                        >{ size }</ToolbarButton>
                    );
                } ) }
            </ToolbarGroup>
        );
        const AlignPanel = () => (
            <PanelBody
                title={ __( 'Column size', 'mydomain' ) }
                initialOpen={ false }
            >
                <SelectControl
                    label={ __( 'Horizontal align', 'mydomain' ) }
                    value={ alignX }
                    options={ [
                        { value: 'start', label: __( 'Image on the right', 'mydomain' ) },
                        { value: 'end', label: __( 'Image on the left', 'mydomain' ) },
                    ] }
                    onChange={ ( value ) => setAttributes( { alignX: value } ) }
                />
                <SelectControl
                    label={ __( 'Vertical align', 'mydomain' ) }
                    value={ alignY }
                    options={ [
                        { value: 'start', label: __( 'Image on the top', 'mydomain' ) },
                        { value: 'end', label: __( 'Image on the bottom', 'mydomain' ) },
                    ] }
                    onChange={ ( value ) => setAttributes( { alignY: value } ) }
                />
            </PanelBody>
        );
        const ColumnPanel = () => (
            <PanelBody
                title={ __( 'Column size', 'mydomain' ) }
                initialOpen={ false }
            >
                <BreakpointButtons/>

                <RangeControl
                    label={ __( 'Image columns', 'mydomain' ) }
                    value={ mediaSpanCols }
                    onChange={ onChangeColumns }
                    min={ 1 }
                    max={ colMaxAmount }
                />

                <RangeControl
                    label={ __( 'Overlay columns', 'mydomain' ) }
                    value={ overlaySpanCols }
                    onChange={ onChangeColumnsOverlay }
                    min={ 1 }
                    max={ colMaxAmount }
                />

            </PanelBody>
        );

        const Media = () => (
            <Fragment>
                <MediaUploadCheck>
                    <MediaUpload
                        onSelect={ onSelectMedia }
                        value={ mediaId }
                        allowedTypes={ [ 'image' ] }
                        render={ ( { open } ) => (
                            <Button
                                className={ mediaId === 0 ? 'editor-post-featured-image__toggle' : 'editor-post-featured-image__preview' }
                                onClick={ open }
                            >
                                { mediaId === 0 && __( 'Choose an image', 'mydomain' ) }
                                { mediaUrl !== undefined &&

                                <img alt={ '' } src={ mediaUrl }/>
                                }
                            </Button>
                        ) }
                    />
                </MediaUploadCheck>
                {
                    mediaId !== 0 &&
                    <MediaUploadCheck>
                        <MediaUpload
                            title={ __( 'Replace image', 'mydomain' ) }
                            value={ mediaId }
                            onSelect={ onSelectMedia }
                            allowedTypes={ [ 'image' ] }
                            render={ ( { open } ) => (
                                <Button
                                    onClick={ open }
                                    isDefault
                                    isLarge>{ __( 'Replace image', 'mydomain' ) }
                                </Button>
                            ) }
                        />
                    </MediaUploadCheck>
                }
                {
                    mediaId !== 0 &&
                    <MediaUploadCheck>
                        <Button
                            onClick={ removeMedia }
                            isLink
                            isDestructive>{ __( 'Remove image', 'mydomain' ) }
                        </Button>
                    </MediaUploadCheck>
                }
            </Fragment>
        );

        const Overlay = () => (
            <InnerBlocks
                templateLock={ false }
                renderAppender={
                    hasChildBlocks ?
                        undefined :
                        () => <InnerBlocks.ButtonBlockAppender/>
                }
            />
        )
        
        return (
            <Fragment>

                <InspectorControls>
                    <AlignPanel/>
                    <ColumnPanel/>
                </InspectorControls>

                <PreviewButton/>

                { ! preview &&
                <div className={ `${ className }` } style={ { display: 'flex' } }>
                    <div style={ { width: 50 + '%' } }>
                        <Media />
                    </div>

                    <div style={ { width: 50 + '%' } }>
                        <Overlay />
                    </div>

                </div> }

                { preview &&
                <MyServerSideRender /> }

            </Fragment>
        );
    }
}

export default compose(
    withSelect

    ( (
        select
        ,
        ownProps
    ) => {
        const {
            clientId
        }
            = ownProps;
        const { getBlockOrder } =
        select( 'core/block-editor' ) || select( 'core/editor' ); // Fallback to 'core/editor' for backwards compatibility

        return {
            hasChildBlocks: getBlockOrder( clientId ).length > 0,
        };
    } )
)
( BlockImageOverlayEdit );

3
  • ServerSideRender uses the Block Renderer API endpoint which does not parse inner blocks, hence the $content would be empty. But why are you using ServerSideRender? Can we see your edit() function?
    – Sally CJ
    Mar 30 at 23:05
  • @SallyCJ thank for the reply. That's a bummer. Then, would you know of a way to render the inner blocks in the editor at edit() as a string/html ? Than I could set it as a block attribute which can be send to the ServerSideRender and use it there. Afterwards I could reset the attribute to empty at save(). Maybe a hacky workaround : ) Yes, in theory I could do all in edit() But I now have two columns in the edit, one for an image and one for innerblocks. They calculate an overlay based on 5 attibutes and breakpoints. It would seem overkill to programme it twice. (frontend/backend)
    – Tim
    Mar 31 at 6:02
  • @SallyCJ To answer your last question, I've now added my current edit code
    – Tim
    Mar 31 at 6:14
2

ServerSideRender uses the Block Renderer API endpoint (/wp/v2/block-renderer/<name>) which does not parse inner blocks, hence the $content in your block render callback would naturally be empty.

However, retrieving the inner blocks is pretty easy and as you've thought, you can set the blocks (or their content) as an attribute that you would pass to ServerSideRender.

But you don't need to save the attribute (using setAttributes()), so it will act just as a "dummy" attribute that you would use with ServerSideRender so that the block render callback would receive the inner blocks.

Working Example

  1. So I'm using innerContent as the name for the dummy attribute, but just use any name that you like, and make sure to register the attribute in PHP. E.g.

    register_block_type( 'mydomain/block-wp-block-image-overlay-01', array(
        'render_callback' => 'my_block_render_callback',
        'attributes'      => array(
            // the dummy attribute
            'innerContent' => array(
                'type'    => 'string',
                'default' => '',
            ),
    
            // your other attributes
            'editorSize'   => array(
                'type'    => 'string',
                'default' => 'md',
            ),
            // ...
        ),
        // ...
    ) );
    

    But in JS, you don't need to actually add the attribute in the attributes property of your block type (that's registered using registerBlockType()).

  2. Then in your MyServerSideRender, retrieve the content of the inner blocks and include the content in the attributes property of the ServerSideRender element:

    Note: I'm using POST as the httpMethod because that method will allow a bigger attributes object.

    // ... your code here.
    const { compose } = wp.compose;
    // 1. After the above line, add the following:
    
    // These are used to retrieve the inner block content.
    const { getBlock } = select( 'core/block-editor' );
    const { getBlockContent } = wp.blocks;
    
    // 2. Then define MyServerSideRender like so:
    const MyServerSideRender = () => {
        // Retrieve the inner block content.
        const innerContent = getBlockContent( getBlock( clientId ) );
    
        // Then append the innerContent to the attributes object below.
        return (
            <ServerSideRender
                block="mydomain/block-wp-block-image-overlay-01"
                attributes={ { ...attributes, innerContent } }
                httpMethod="POST"
            />
        );
    };
    
  3. And then in the block render callback, you would use the dummy (innerContent) attribute like so:

    function my_block_render_callback( $attributes, $content ) {
        // On the front-end (or non REST API requests), we use $content.
    
        if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
            return '<h3>MyServerSideRender output</h3>' . $attributes['innerContent'];
        }
        return $content;
    
        /* Or you could instead just do:
        return $content ? $content : $attributes['innerContent'];
        */
    }
    

So try those and let me know how it goes? =)

2
  • Thank you @sally-cj ! That did the trick. I had that contend before in the editor via a different route, both as a token string and as an object. Where I screwed up the most part was to try and parse it in the callback. I've tried render_block() on different ways, but I kept getting 401 errrors... Oh man, all it needed was to return the value.... :) Sjees thanks. I already gave up.
    – Tim
    Apr 1 at 5:52
  • Well, I guess I should've asked for your render_callback code, eh? :) Anyway, I'm glad my answer helped!
    – Sally CJ
    Apr 1 at 12:10

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.