-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Server Side Render component. #5602
Changes from 20 commits
cd58776
b0ede76
340136e
514dd4f
a90f40d
0c127e1
b0b977a
395a168
1d05c2d
c40dd25
f323651
c4abc69
6557877
228c756
b48ef81
e39332d
46506a9
ce1f7c6
e0fec83
17e404a
39b867a
4c3129b
3c60d4b
a10bfac
a166324
f0f4a77
6d1ee65
2bbbeb7
6f7d62e
67af6b7
007fb1d
1c9ef56
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
ServerSideRender | ||
======= | ||
|
||
ServerSideRender component is used for server-side rendering preview in Gutenberg editor, specifically for dynamic blocks. Server-side rendering in a block's `edit` function should be limited for blocks which are heavily dependent on (existing) PHP rendering logic that is heavily intertwined with data, such as when there are no endpoints available. | ||
|
||
New blocks should be built in conjunction with any necessary REST API endpoints so that JavaScript can be used for rendering client-side in the `edit` function for the best user experience, instead of relying on using the PHP `render_callback`. The logic necessary for rendering should be included in the endpoint so that both the client-side JS and server-side PHP logic should require a mininal amount of differences. | ||
|
||
## Usage | ||
|
||
Render core/archives preview. | ||
```jsx | ||
<ServerSideRender | ||
block="core/archives" | ||
attributes={ this.props.attributes } | ||
/> | ||
``` | ||
|
||
## Output | ||
|
||
Output is using the `render_callback` set when defining the block. For example if `block="core/archives"` as in the example then the output will match `render_callback` output of that block. | ||
|
||
## API Endpoint | ||
API endpoint for getting the output for ServerSideRender is `/gutenberg/v1/block-renderer/:block`. It accepts any params which are used as `attributes` for the block's `render_callback` method. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
/** | ||
* External dependencies. | ||
*/ | ||
import { isEqual, isObject, map } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { | ||
Component, | ||
RawHTML, | ||
} from '@wordpress/element'; | ||
import { __ } from '@wordpress/i18n'; | ||
|
||
export class ServerSideRender extends Component { | ||
constructor( props ) { | ||
super( props ); | ||
this.state = { | ||
response: null, | ||
}; | ||
} | ||
|
||
componentDidMount() { | ||
this.fetch( this.props ); | ||
} | ||
|
||
componentWillReceiveProps( nextProps ) { | ||
if ( ! isEqual( nextProps, this.props ) ) { | ||
this.fetch( nextProps ); | ||
} | ||
} | ||
|
||
fetch( props ) { | ||
this.setState( { response: null } ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should check if
https://reactjs.org/docs/react-component.html#componentdidmount There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed within #6571. |
||
const { block, attributes } = props; | ||
|
||
const path = '/gutenberg/v1/block-renderer/' + block + '?' + this.getQueryUrlFromObject( { attributes } ); | ||
|
||
return wp.apiRequest( { path: path } ).then( response => { | ||
if ( response && response.rendered ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's nothing to say that the component is still mounted by the time this response is received, so attempting to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed within #6571. |
||
this.setState( { response: response.rendered } ); | ||
} | ||
} ); | ||
} | ||
|
||
getQueryUrlFromObject( obj, prefix ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sounds like a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In fact, it could probably be built-in as the default behavior for the existing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the behavior is needed, it could be added to the existing function via pull request to the packages repository. |
||
return map( obj, ( paramValue, paramName ) => { | ||
const key = prefix ? prefix + '[' + paramName + ']' : paramName, | ||
value = obj[ paramName ]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed within #6571. |
||
return isObject( paramValue ) ? this.getQueryUrlFromObject( value, key ) : | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe not intentional:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed within #6571. |
||
encodeURIComponent( key ) + '=' + encodeURIComponent( value ); | ||
} ).join( '&' ); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
import { map } from 'lodash';
return map( obj, ( paramValue, paramName ) => {
const key = prefix ? …;
return isObject( paramValue ) ? this.getQ… : encodeURI…;
} ).join( '&' );
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you, replaced the method in e0fec83. |
||
|
||
render() { | ||
const response = this.state.response; | ||
if ( ! response || ! response.length ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed within #6571, let me know if it looks OK now. |
||
return ( | ||
<div key="loading" className="wp-block-embed is-loading"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For what reason do we need a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replaced with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't reuse classes but components. If there's something in the way the embed blocks handles the loading state that is useful, it should be extracted in a separate UI component. |
||
|
||
<p>{ __( 'Loading...' ) }</p> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need both a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replaced with |
||
</div> | ||
); | ||
} | ||
|
||
return ( | ||
<RawHTML key="html">{ response }</RawHTML> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess not because then styles won't be applied. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @westonruter Sandbox doesn't stop styles being applied (If I understand correctly it ensures no css / js bleeds into admin experience). It's used in #4710. Should work on such block-specifics defer to this once it's released? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean the CSS from the editor. Otherwise, the CSS has to be re-printed in the iframe. |
||
); | ||
} | ||
} | ||
|
||
export default ServerSideRender; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
<?php | ||
/** | ||
* Block Renderer REST API: WP_REST_Block_Renderer_Controller class | ||
* | ||
* @package gutenberg | ||
* @since ? | ||
*/ | ||
|
||
/** | ||
* Controller which provides REST endpoint for rendering a block. | ||
* | ||
* @since ? | ||
* | ||
* @see WP_REST_Controller | ||
*/ | ||
class WP_REST_Block_Renderer_Controller extends WP_REST_Controller { | ||
|
||
/** | ||
* Constructs the controller. | ||
* | ||
* @access public | ||
*/ | ||
public function __construct() { | ||
// @codingStandardsIgnoreLine - PHPCS mistakes $this->namespace for the namespace keyword. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we ignore this in the ruleset? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added an exception for this file within 6d1ee65. |
||
$this->namespace = 'gutenberg/v1'; | ||
$this->rest_base = 'block-renderer'; | ||
} | ||
|
||
/** | ||
* Registers the necessary REST API routes, one for each dynamic block. | ||
* | ||
* @access public | ||
*/ | ||
public function register_routes() { | ||
$block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); | ||
foreach ( $block_types as $block_type ) { | ||
if ( ! $block_type->is_dynamic() ) { | ||
continue; | ||
} | ||
|
||
// @codingStandardsIgnoreLine - PHPCS mistakes $this->namespace for the namespace keyword. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto above. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added an exception for this file within 6d1ee65. |
||
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<name>' . $block_type->name . ')', array( | ||
'args' => array( | ||
'name' => array( | ||
'description' => __( 'Unique registered name for the block.', 'gutenberg' ), | ||
'type' => 'string', | ||
), | ||
), | ||
array( | ||
'methods' => WP_REST_Server::READABLE, | ||
'callback' => array( $this, 'get_item' ), | ||
'permission_callback' => array( $this, 'get_item_permissions_check' ), | ||
'args' => array( | ||
'context' => $this->get_context_param( array( 'default' => 'view' ) ), | ||
'attributes' => array( | ||
/* translators: %s is the name of the block */ | ||
'description' => sprintf( __( 'Attributes for %s block', 'gutenberg' ), $block_type->name ), | ||
'type' => 'object', | ||
'additionalProperties' => false, | ||
'properties' => $block_type->attributes, | ||
), | ||
), | ||
), | ||
'schema' => array( $this, 'get_public_item_schema' ), | ||
) ); | ||
} | ||
} | ||
|
||
/** | ||
* Checks if a given request has access to read blocks. | ||
* | ||
* @since ? | ||
* @access public | ||
* | ||
* @param WP_REST_Request $request Request. | ||
* @return true|WP_Error True if the request has read access, WP_Error object otherwise. | ||
*/ | ||
public function get_item_permissions_check( $request ) { | ||
if ( ! current_user_can( 'edit_posts' ) ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something that comes to mind is that some blocks will want to render differently depending on their post context. In that case, there should be an At least this is the case for shortcodes, for example a The author bio block in #3250 is another example for where for there to be a post context to render. Should the post dependency be formalized as a new attribute source type to make the explicit and declarative that the block depends on a post context to be present? In the case of the author bio block, this could be formalized as follows instead of relying on attributes: {
author: {
type: 'int',
source: 'post-context',
field: 'author'
},
}, This would allow for the attribute's source to be matched in a server-side context as well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @westonruter To see if I understand the thoughts correctly: On the new There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I commented on the I see this being analogous to the So then the topic of the new attribute postAuthorId: getEditedPostAttribute( state, 'author' ), The impact of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, now I think I understand -- the issue of not having In the context of SSR -- I guess we would then need to check if there are attributes that require There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @westonruter Added setting the global |
||
return new WP_Error( 'gutenberg_block_cannot_read', __( 'Sorry, you are not allowed to read Gutenberg blocks as this user.', 'gutenberg' ), array( | ||
'status' => rest_authorization_required_code(), | ||
) ); | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* Returns block output from block's registered render_callback. | ||
* | ||
* @since ? | ||
* @access public | ||
* | ||
* @param WP_REST_Request $request Full details about the request. | ||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. | ||
*/ | ||
public function get_item( $request ) { | ||
$registry = WP_Block_Type_Registry::get_instance(); | ||
$block = $registry->get_registered( $request['name'] ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you return an error here if the block is invalid? |
||
$data = array( | ||
'rendered' => $block->render( $request->get_param( 'attributes' ) ), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @westonruter This isn't working with the current SSR component code since the attributes are all sent as separate params and not as one
Just sending There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good question. I think it's important for const apiURL = addQueryArgs( '/gutenberg/v1/block-renderer/' + block, {
attributes,
_wpnonce: wpApiSettings.nonce,
} ); In other words, I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Couldn't find an existing method at this moment, apparently query-string is intentionally not supporting nested attributes and suggests sending the object as a JSON string. Added a custom method for now to the class to put together the query string supporting objects ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We may also be passing the full block content to the render callback in #6239 Rather than needing to remember passing all supported arguments to the |
||
); | ||
return rest_ensure_response( $data ); | ||
} | ||
|
||
/** | ||
* Retrieves block's output schema, conforming to JSON Schema. | ||
* | ||
* @since ? | ||
* @access public | ||
* | ||
* @return array Item schema data. | ||
*/ | ||
public function get_item_schema() { | ||
return array( | ||
'$schema' => 'http://json-schema.org/schema#', | ||
'title' => 'rendered-block', | ||
'type' => 'object', | ||
'properties' => array( | ||
'rendered' => array( | ||
'description' => __( 'The rendered block.', 'gutenberg' ), | ||
'type' => 'string', | ||
'required' => true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added within a10bfac. |
||
), | ||
), | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's important to stress two other conditions to prefer SSR, more significant, I dare say, than the logic/data entanglement, since that isn't of itself insurmountable:
The ultimate point, perhaps, is that—however useful—SSR should be regarded as a fallback.