Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions packages/block-library/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import {
store as blocksStore,
} from '@wordpress/blocks';
import { select } from '@wordpress/data';
import { createElement } from '@wordpress/element';
import ServerSideRender from '@wordpress/server-side-render';
import { useBlockProps } from '@wordpress/block-editor';
import { useServerSideRender } from '@wordpress/server-side-render';
import { __, sprintf } from '@wordpress/i18n';

/**
* Internal dependencies
Expand Down Expand Up @@ -326,11 +327,39 @@ export const registerCoreBlocks = (
registerBlockType( blockName, {
title: blockName,
...( bootstrappedApiVersion < 3 && { apiVersion: 3 } ),
edit: ( { attributes } ) => {
return createElement( ServerSideRender, {
edit: function Edit( { attributes } ) {
const blockProps = useBlockProps();
const { content, status, error } = useServerSideRender( {
block: blockName,
attributes,
} );

if ( status === 'loading' ) {
return (
<div { ...blockProps }>{ __( 'Loading…' ) }</div>
);
}

if ( status === 'error' ) {
return (
<div { ...blockProps }>
{ sprintf(
/* translators: %s: error message describing the problem */
__( 'Error loading block: %s' ),
error
) }
</div>
);
}

return (
<div
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there auto-registered blocks who's wrapper is not a div?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related question, is this div also present in the frontend or is this going to result in a different DOM tree than frontend?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only for the editor; the frontend only shows the content of the render callback. I don't think it's a necessity to allow other wrappers, unless we add an optional attribute to allow a limited set of wrappers (e.g., figure)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is an issue for all blocks that use "server sider render" component or hook. I wonder if there's a way to unify the DOM trees. A question for later.

Copy link
Contributor Author

@priethor priethor Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the HTML of a test block followed by a regular table block, the

wrapper is only in the editor:

Editor Frontend
image image

Interestingly, the table uses the same wrapper in both, so as you say, it seems to be how the SSR hook works.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that one of the reasons for not recommending the ServerSideRender package (or useServerSideRender hook) is that it creates the extra div wrapper in the Edit component. It would be great if we could make the DOM tree perfectly match on both the frontend and the editor. Do you guess this is technically possible?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might be able to use a library like this

https://github.com/remarkablemark/html-react-parser

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's possible to try something like this yes, but I don't believe that's the reason for not using client side edit, client site edit is more about tailored UX.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client site edit is more about tailored UX.

I'm curious about this, for example, creating a React component dynamically based on a string defined on the PHP side?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about this, for example, creating a React component dynamically based on a string defined on the PHP side?

No I meant that we don't recommend ServerSideRender because the ideal experience is that the block in the editor is interactive and updates as you make block modifications without reaching the server, which is not the case for ServerSiteRender.


That said, that doesn't stop us from making ServerSideRender better for those who use it and server side registered block. And using a library like you shared could be a good path forward to make the editor markup match the frontend.

{ ...blockProps }
dangerouslySetInnerHTML={ {
__html: content || '',
} }
/>
);
},
save: () => null,
} );
Expand Down
18 changes: 16 additions & 2 deletions packages/e2e-tests/plugins/server-side-rendered-block.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,25 @@ static function () {
register_block_type(
'test/auto-register-block',
array(
'render_callback' => static function () {
return '<div>Auto-register block content</div>';
'render_callback' => static function ( $attributes ) {
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => 'auto-register-example',
)
);

return sprintf(
'<div %1$s><p>Auto-register block content</p><p>Background: %2$s</p></div>',
$wrapper_attributes,
isset( $attributes['backgroundColor'] ) ? esc_html( $attributes['backgroundColor'] ) : 'default'
);
},
'supports' => array(
'auto_register' => true,
'color' => array(
'background' => true,
'text' => false,
),
),
)
);
Expand Down
41 changes: 40 additions & 1 deletion test/e2e/specs/editor/plugins/server-side-rendered-block.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,53 @@ test.describe( 'PHP-only auto-register blocks', () => {
expect( blockExists ).toBe( false );
} );

test( 'should render server-side content for auto-registered blocks', async ( {
test( 'should render server-side content for auto-registered blocks with block supports', async ( {
editor,
page,
} ) => {
await editor.insertBlock( {
name: 'test/auto-register-block',
} );

const block = editor.canvas.getByText( 'Auto-register block content' );
await expect( block ).toBeVisible();

// Verify default background value is shown
const backgroundText = editor.canvas.getByText( 'Background: default' );
await expect( backgroundText ).toBeVisible();

// Verify block has proper wrapper classes
const blockWrapper = editor.canvas.locator( '.auto-register-example' );
await expect( blockWrapper ).toBeVisible();

// Verify the block wrapper has the wp-block class applied
const wpBlockClass = editor.canvas
.locator( '.wp-block-test-auto-register-block' )
.first();
await expect( wpBlockClass ).toBeVisible();

// Change the background color and get its name
await blockWrapper.click();
await editor.openDocumentSettingsSidebar();

const colorsButton = page.getByRole( 'button', { name: /Color/i } );
await colorsButton.click();

const backgroundButton = page.getByRole( 'button', {
name: /Background/i,
} );
await backgroundButton.click();
const firstColor = page.getByRole( 'option' ).first();
const colorName = await firstColor.getAttribute( 'aria-label' );
await firstColor.click();

// Verify background is no longer 'default'
await expect( backgroundText ).toBeHidden();

// Verify the selected color appears in the block content
const colorText = editor.canvas.getByText(
`Background: ${ colorName.toLowerCase().replace( /\s+/g, '-' ) }`
);
await expect( colorText ).toBeVisible();
} );
} );
Loading