WP Blocks - Gutenberg - <ServerSideRender/> not rendering $content
-
26-04-2021 - |
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 );
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
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 usingregisterBlockType()
).Then in your
MyServerSideRender
, retrieve the content of the inner blocks and include the content in theattributes
property of theServerSideRender
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" /> ); };
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? =)