Question

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 );

Was it helpful?

Solution

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? =)

Licensed under: CC-BY-SA with attribution
Not affiliated with wordpress.stackexchange
scroll top